本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~
單例模式(Singleton),目的是為了保證在一個程序中,某個類有且僅有一個範例。
由於這個類只有一個範例,所以不能讓呼叫方使用new Xxx()
來建立範例。所以,單例的構造方法必須是private
,這樣就防止了呼叫方自己建立範例。
單例模式的實現需要三個必要的條件:
另外,實現單例類時,還需要考慮三個問題:
下面介紹幾種實現單例模式的方式。
JVM在類的初始化階段,會執行類的靜態方法。在執行類的初始化期間,JVM會去獲取Class物件的鎖。這個鎖可以同步多個執行緒對同一個類的初始化。
餓漢模式只在類載入的時候建立一次範例,沒有多執行緒同步的問題。單例沒有用到也會被建立,而且在類載入之後就被建立,記憶體就被浪費了。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton newInstance() {
return instance;
}
}
餓漢式單例的優點:
餓漢式單例的缺點:單例物件的建立,不是延時載入。
與餓漢式思想不同,懶漢式支援延時載入,將物件的建立延遲到了獲取物件的時候。不過為了執行緒安全,在獲取物件的操作需要加鎖,這就導致了低效能。
public class Singleton {
private static final Singleton instance;
private Singleton () {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上述程式碼加的鎖只有在第一次建立物件時有用,而之後每次獲取物件,其實是不需要加鎖的(雙重檢查鎖定優化了這個問題)。
懶漢式單例優點:
懶漢式單例缺點:
雙重檢查鎖定將懶漢式中的 synchronized
方法改成了 synchronized
程式碼塊。如下:
public class Singleton {
private static volatile Singleton instance = null; //volatile
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
雙重校驗鎖先判斷 instance 是否已經被範例化,如果沒有被範例化,那麼才對範例化語句進行加鎖。
instance使用static修飾的原因:getInstance為靜態方法,因為靜態方法的內部不能直接使用非靜態變數,只有靜態成員才能在沒有建立物件時進行初始化,所以返回的這個範例必須是靜態的。
為什麼兩次判斷instance == null
:
Time | Thread A | Thread B |
---|---|---|
T1 | 檢查到instance 為空 |
|
T2 | 檢查到instance 為空 |
|
T3 | 初始化物件A |
|
T4 | 返回物件A |
|
T5 | 初始化物件B |
|
T6 | 返回物件B |
new Singleton()
會執行三個動作:分配記憶體空間、初始化物件和物件參照指向記憶體地址。
memory = allocate(); // 1:分配物件的記憶體空間
ctorInstance(memory); // 2:初始化物件
instance = memory; // 3:設定instance指向剛分配的記憶體地址
由於指令重排優化的存在,導致初始化物件和將物件參照指向記憶體地址的順序是不確定的。在某個執行緒建立單例物件時,會為該物件分配了記憶體空間並將物件的欄位設定為預設值。此時就可以將分配的記憶體地址賦值給instance欄位了,然而該物件可能還沒有初始化。若緊接著另外一個執行緒來呼叫getInstance,取到的是未初始化的物件,程式就會出錯。volatile 可以禁止指令重排序,保證了先初始化物件再賦值給instance變數。
Time | Thread A | Thread B |
---|---|---|
T1 | 檢查到instance 為空 |
|
T2 | 獲取鎖 | |
T3 | 再次檢查到instance 為空 |
|
T4 | 為instance 分配記憶體空間 |
|
T5 | 將instance 指向記憶體空間 |
|
T6 | 檢查到instance 不為空 |
|
T7 | 存取instance (此時物件還未完成初始化) |
|
T8 | 初始化instance |
雙重檢查鎖定單例優點:
它與餓漢模式一樣,也是利用了類初始化機制,因此不存在多執行緒並行的問題。不一樣的是,它是在內部類裡面去建立物件範例。這樣的話,只要應用中不使用內部類,JVM就不會去載入這個單例類,也就不會建立單例物件,從而實現懶漢式的延遲載入。也就是說這種方式可以同時保證延遲載入和執行緒安全。
基於類初始化的方案的實現程式碼更簡潔。
public class Instance {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
private Instance() {}
public static Instance getInstance() {
return InstanceHolder.instance ; // 這裡將導致InstanceHolder類被初始化
}
}
如上述程式碼,InstanceHolder
是一個靜態內部類,當外部類 Instance
被載入的時候,並不會建立 InstanceHolder
範例物件。
只有當呼叫 getInstance()
方法時,InstanceHolder
才會被載入,這個時候才會建立 Instance
。Instance
的唯一性、建立過程的執行緒安全性,都由 JVM 來保證。
靜態內部類單例優點:
用列舉來實現單例,是最簡單的方式。這種實現方式通過 Java 列舉型別本身的特性,保證了範例建立的執行緒安全性和範例的唯一性。
public enum Singleton {
INSTANCE; // 該物件全域性唯一
}
最後給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~