一、餓漢式單例類
}
private static Singleton instance = new Singleton();
private static Singleton getInstance(){
return instance;
}
}
特點:餓漢式提前實例化,沒有懶漢式中多執行緒問題,但不管我們是不是呼叫getInstance()都會存在一個實例在記憶體中
二、內部類式單例類
}
private class SingletonHoledr(){
private static Singleton instance = new Singleton();
}
private static Singleton getInstance(){
return SingletonHoledr.instance;
}
}
特點:內部類別中,實現了延遲加載,只有我們調用了getInstance(),才會創建唯一的實例到內存中.並且也解決了懶漢式中多線程的問題.解決的方式是利用了Classloader的特性.
三、懶漢式單例類
}
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null){
return instance = new Singleton();
}else{
return instance;
}
}
}
特點:在懶漢式中,有執行緒A和B,當執行緒A運行到第8行時,跳到執行緒B,當B也運行到8行時,兩個執行緒的instance都為空,這樣就會生成兩個實例。解決的辦法是同步:
可以同步但是效率不高:
}
private static Singleton instance;
public static synchronized Singleton getInstance(){
if(instance == null){
return instance = new Singleton();
}else{
return instance;
}
}
}
這樣寫程式不會出錯,因為整個getInstance是一個整體的"critical section",但就是效率很不好,因為我們的目的其實只是在第一個初始化instance的時候需要locking(加鎖),而後面取用instance的時候,根本不需要執行緒同步。
於是聰明的人們想出了下面的做法:
雙檢鎖寫法:
public static Singleton getSingle(){ //外部透過此方法可以取得對象
if(single == null){
synchronized (Singleton.class) { //保證了同一時間只能有一個物件存取此同步區塊
if(single == null){
single = new Singleton();
}
}
}
return single; //傳回建立好的對象
}
}
想法很簡單,就是我們只需要同步(synchronize)初始化instance的那部分程式碼從而使程式碼既正確又很有效率。
這就是所謂的「雙檢鎖」機制(顧名思義)。
很可惜,這樣的寫法在很多平台和最佳化編譯器上都是錯誤的。
原因在於:instance = new Singleton()這行程式碼在不同編譯器上的行為是無法預測的。一個最佳化編譯器可以合法地如下實作instance = new Singleton():
1. instance = 給新的實體分配內存
2. 呼叫Singleton的建構子來初始化instance的成員變數
現在想像一下有線程A和B在呼叫getInstance,線程A先進入,執行到步驟1的時候就被踢出了cpu。然後線程B進入,B看到的是instance 已經不是null了(內存已經分配),於是它開始放心地使用instance,但這個是錯誤的,因為在這一時刻,instance的成員變量還都是缺省值,A還來不及執行步驟2來完成instance的初始化。
當然編譯器也可以這樣實作:
1. temp = 分配內存
2. 呼叫temp的建構函數
3. instance = temp
如果編譯器的行為是這樣的話我們似乎就沒有問題了,但事實卻不是那麼簡單,因為我們無法知道某個編譯器具體是怎麼做的,因為在Java的memory model裡對這個問題沒有定義。
雙檢鎖對於基礎型別(如int)適用。很顯然吧,因為基礎型別沒有呼叫建構函式這一步。