2019獨角獸企業重金招聘Python工程師標準>>>
Java單例的常見形式
本文目的:總結Java中的單例模式
本文定位:學習筆記
學習過程記錄,加深理解,便于回顧。也希望能給學習的同學一些靈感
一、非延遲加載單例類
public class Singleton {private Singleton() {}private static final Singleton instance = new Singleton();public static Singleton getInstance() {return instance;}
}
二、同步延遲加載
public class Singleton {private static Singleton instance = null;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
三、雙重檢測同步延遲加載
為了減少同步的開銷,于是有了雙重檢查模式
為處理原版非延遲加載方式瓶頸問題,我們需要對 instance 進行第二次檢查,目的是避開過多的同步(因為這里的同步只需在第一次創建實例時才同步,一旦創建成功,以后獲取實例時就不需要同獲取鎖了),但在Java中行不通,因為同步塊外面的if (instance == null)可能看到已存在,但不完整的實例。
public class Singleton {private volatile static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized(Singleton.class) { // 1 if (instance == null) { // 2 instance = new Singleton(); // 3 }}}return instance;}
}
雙重檢測鎖定失敗的問題并不歸咎于 JVM 中的實現 bug,而是歸咎于 Java 平臺內存模型。內存模型允許所謂的“無序寫入”,這也是失敗的一個主要原因。
無序寫入: 為解釋該問題,需要重新考察上述清單中的 //3 行。此行代碼創建了一個 Singleton 對象并初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行之前,變量 instance 可能成為非 null 的,即賦值語句在對象實例化之前調用,此時別的線程得到的是一個還會初始化的對象,這樣會導致系統崩潰。 什么?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。假設代碼執行以下事件序列:
- 線程 1 進入 getInstance() 方法。
- 由于 instance 為 null,線程 1 在 //1 處進入 synchronized 塊。
- 線程 1 前進到 //3 處,但在構造函數執行之前,使實例成為非 null。
- 線程 1 被線程 2 預占。
- 線程 2 檢查實例是否為 null。因為實例不為 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton 對象。
- 線程 2 被線程 1 預占。
- 線程 1 通過運行 Singleton 對象的構造函數并將引用返回給它,來完成對該對象的初始化。
為展示此事件的發生情況,假設代碼行 instance =new Singleton(); 執行了下列偽代碼:
mem = allocate(); //為單例對象分配內存空間.
instance = mem; //注意,instance 引用現在是非空,但還未初始化
ctorSingleton(instance); //為單例對象通過instance調用構造函數
這段偽代碼不僅是可能的,而且是一些 JIT 編譯器上真實發生的。執行的順序是顛倒的,但鑒于當前的內存模型,這也是允許發生的。JIT 編譯器的這一行為使雙重檢查鎖定的問題只不過是一次學術實踐而已。
在 Java 中雙重檢查模式無效的原因是在不同步的情況下引用類型不是線程安全的。對于除了 long 和 double 的基本類型,雙重檢查模式是適用 的。比如下面這段代碼就是正確的:
private int count;
public int getCount(){ if (count == 0){ synchronized(this){ if (count == 0){ count = computeCount(); //一個耗時的計算 } } } return count;
}
上面就是關于java中雙重檢查模式(double-check idiom)的一般結論。
double-check無效原因可參見
但是事情還沒有結束,因為java的內存模式也在改進中。Doug Lea 在他的文章中寫道:“根據最新的 JSR133 的 Java 內存模型,如果將引用類型聲明為 volatile,雙重檢查模式就可以工作了”, 參見 。
Section 2.2.7.x on the Memory Model doesn't provide current details of the JSR133 spec revision. (The basic ideas still apply though.) One change that commonly arises in practice is that Section 2.2.7.4 and 2.4.1.2 should say that reading a volatile reference makes visible other changes to the referred object by the thread writing the reference. In particular, double-check idioms work in the expected way when references are declared volatile.
所以以后要在 Java 中使用雙重檢查模式,可以使用下面的代碼:
private volatile Resource resource;
public Resource getResource(){ if (resource == null){ synchronized(this){ if (resource==null){ resource = new Resource(); } } } return resource;
}
四、使用內部類實現延遲加載(推薦)
推薦方法Initialization-on-demand holder idiom
public class Singleton { static class SingletonHolder { static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; }
}
其他
以上為常見單例形式,另有 ThreadLocal、枚舉 等實現形式
此處不作介紹
參考
- http://www.cnblogs.com/dolphin0520/p/3920373.html#!comments
- http://www.iteye.com/topic/652440
- http://www.iteye.com/topic/260515
- https://blog.csdn.net/dl88250/article/details/5439024
- https://blog.csdn.net/chenchaofuck1/article/details/51702129
對本文有什么建議(內容、寫作風格等),歡迎留言提出,感謝!