概述
單例模式(SingletonPattern),保證一個類僅有一個實體,并提供一個訪問它的全域訪問點,
單例模式有 3 個特點:
- 單例類只有一個實體物件;
- 該單例物件必須由單例類自行創建;
- 單例類對外提供一個訪問該單例的全域訪問點;
在很多比較大型的程式中,全域變數經常被用到,如果不用全域變數,那么在使用到的模塊中,都需要用引數將全域變數傳入,這是非常麻煩的,雖然要減少使用全域變數,但是如果需要,還是要用,單例模式就是對傳統的全域的一種改進,單例可以做到延時實體化,即在需要的時候才進行實體化,針對一些大型的類,延時實體化是有好處的,
實作
餓漢式單例
/**
* 餓漢式單例
* 執行緒安全
*/
public class Singleton1 {
// jvm保證在任何執行緒訪問instance靜態變數之前一定先創建了此實體
private static Singleton1 instance = new Singleton1();
// 私有化構造方法,保證外界無法直接實體化
private Singleton1() {
}
// 提供全域訪問點獲取唯一的實體
public static Singleton1 getInstance() {
return instance;
}
}
- 優點:沒有加鎖,執行效率會提高,
- 缺點:類加載時就初始化,浪費記憶體,
- 場景:這種實作方式適合單例占用記憶體比較小,在初始化時就會被用到的情況,但是,如果單例占用的記憶體比較大,或單例只是在某個特定場景下才會用到,使用餓漢模式就不合適了,這時候就需要用到懶漢模式進行延遲加載,
懶漢式單例
/**
* 雙重檢查單例(懶漢式)
* 執行緒安全
* 單例實體在第一次使用時進行創建
*/
public class Singleton2 {
private volatile static Singleton2 instance = null;
// 私有化建構式
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
// 多執行緒可達,可能存在A實體化釋放鎖后,阻塞在此的B獲得同步鎖,所以此處需要雙重檢測
synchronized (Singleton2.class) {
if (instance == null) {
// 此處的執行順序期望如下:
// 1. memory = allocate() 分配物件的記憶體空間
// 2. ctorInstance() 初始化物件
// 3. instance = memory 設定instance指向剛分配的記憶體
// 如果不用volatile修飾變數, 2、3指令可能重排,導致獲取未初始化的物件
instance = new Singleton2();
}
}
}
return instance;
}
}
- 優點:第一次呼叫才初始化,避免記憶體浪費,
- 缺點:必須加鎖synchronized才能保證單例,(靜態同步方法實作的懶漢式)加鎖會影響效率,
登記式單例
/**
* 靜態內部類單例(登記式、延遲加載)
*/
public class Singleton3 {
private Singleton3() {
}
/**
* 靜態內部類
* 在第一次呼叫getInstance方法之前,SingletonWrapper類是沒有被加載的,因為它是一個靜態內部類,
* 當有執行緒第一次呼叫getInstance的時候,SingletonWrapper就會被class loader加載進JVM,在加載的同時,執行instance的初始化,
* 所以,這種寫法,仍然是一種懶漢式的單例類,
*/
private static class SingletonWrapper {
private static final Singleton3 instance = new Singleton3();
}
/**
* 為什么這樣寫就是執行緒安全的呢?
* 因為類的加載的程序是單執行緒執行的,它的并發安全是由JVM保證的,
* 所以,這樣寫的好處是在instance初始化的程序中,由JVM的類加載機制保證了執行緒安全,
* 而在初始化完成以后,不管后面多少次呼叫getInstance方法都不會再遇到鎖的問題了,
*
* @return
*/
public static Singleton3 getInstance() {
return SingletonWrapper.instance;
}
}
- 優點: 內部類只有在外部類被呼叫才加載,產生SINGLETON實體;又不用加鎖,此模式有上述兩個模式的優點,屏蔽了它們的缺點,是推薦的單例模式,
- 缺點: 在實體需要序列化的場景下,反射和序列化會破壞單例,這是懶漢式、餓漢式和登記式共同存在的缺陷,
列舉單例
/**
* 列舉單例
* 執行緒安全
*/
public class Singleton4 {
// 私有建構式
private Singleton4() {
}
public static Singleton4 getInstance() {
return Singleton.INSTANCE.getInstance();
}
// 列舉實體是static final型別的,也就表明只能被實體化一次,
// 在呼叫構造方法時,我們的單例被實體化
private enum Singleton {
INSTANCE;
private Singleton4 singleton;
// JVM保證這個方法絕對只呼叫一次
Singleton() {
singleton = new Singleton4();
}
public Singleton4 getInstance() {
return singleton;
}
}
}
- 列舉提供了序列化機制,推薦的
最佳實作方式
反射和反序列化對單例的影響
通過反射來實體化類
/**
* 用反射來獲得實體
*/
public class Singleton5 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<Singleton1> clz = Singleton1.class;
Constructor<Singleton1> constructor = clz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton1 reflectInstance = constructor.newInstance();
Singleton1 instance = Singleton1.getInstance();
System.out.println(reflectInstance == instance); // false
}
}
結果輸出false,說明reflectInstance和instance不是同一個物件,(==比較的是實體物件的記憶體地址)
通過反序列化來實體化類
/**
* 反序列化來獲得實體
*/
public class Singleton6 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 單例(此處對單例進行修改,實作Serializable介面)
Singleton1 singleton = Singleton1.getInstance();
// 序列化
FileOutputStream fos = new FileOutputStream("Singleton1.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton);
oos.flush();
fos.close();
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("Singleton1.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton1 instance = (Singleton1) ois.readObject();
fis.close();
ois.close();
// 對比
System.out.println(singleton == instance); // false
}
}
結果輸出false,說明singleton和instance指向不同物件,
如何避免單例被破壞
修改單例類,解決反序列化的問題
/**
* 餓漢式單例
* 執行緒安全
*/
public class Singleton1 implements Serializable {
// jvm保證在任何執行緒訪問instance靜態變數之前一定先創建了此實體
private static Singleton1 instance = new Singleton1();
// 私有化構造方法,保證外界無法直接實體化
private Singleton1() {
}
// 提供全域訪問點獲取唯一的實體
public static Singleton1 getInstance() {
return instance;
}
//該方法在反序列化時會被呼叫,該方法不是介面定義的方法,有點兒約定俗成的感覺
protected Object readResolve() throws ObjectStreamException {
System.out.println("呼叫了readResolve方法!");
return instance;
}
}
結果輸出
呼叫了readResolve方法!
true
應用場景
單例模式可以避免實體物件的重復創建,不僅可以減少每次創建物件的時間開銷,還可以節約記憶體空間,有以下場景的特點即可使用單例,
當物件需要被共享的場合,由于單例模式只允許創建一個物件,共享該物件可以節省記憶體,并加快物件訪問速度,如資料庫的連接池、zK分布式鎖、工具類等,
當某類需要頻繁實體化,而創建的物件又頻繁被銷毀的時候,如多執行緒的執行緒池、網路連接池等,
公眾號 【當我遇上你】

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/22714.html
標籤:設計模式
上一篇:10 分鐘從零搭建個人博客
