01. 單例模式
1. 介紹與比較
我們經常看到的單例模式,按加載時機可以分為:餓漢方式和懶漢方式;按實現的方式,有:synchronized修飾方法、雙重檢查加鎖,內部類方式和枚舉方式等等。另外還有一種通過Map容器來管理單例的方式。
2. 雙重檢查鎖定的Bug
今天寫了一個工具類,以單例的形式持有內部具體處理類的引用。
public class LogProcessorUtils {
private static LogProcessorInterface logProcessor = null;
private LogProcessorUtils() {
}
public static String processLog2Json(String s) {
// 保證logProcessor單例
if (logProcessor == null) {
synchronized (LogProcessorUtils.class) {
if (logProcessor == null) {
logProcessor = new LogProcessorImpl();
}
}
}
return logProcessor.process(s);
}
}
但是,在FindBugs插件中爆出bug。以下是網頁中的介紹:
- DC: Possible double check of field (DC_DOUBLECHECK)
This method may contain an instance of double-checked locking. This idiom is not correct according to the semantics of the Java memory model. For more information, see the web page http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.
大致意思是,以雙重檢查鎖定實現的單例,在理論是正確的,但是實際上,因為Java內存模型的原因,可能造成錯誤,不推薦使用。
網上查閱了資料,參考《http://blog.csdn.net/chenchaofuck1/article/details/51702129》
解釋如下:
public static Singleton getInstance(){
if (instance == null){
synchronized(Singleton.class) { //1
if (instance == null) //2
instance = new Singleton(); //3
}
}
return instance;
}
雙重檢查鎖定背后的理論是:在 //2 處的第二次檢查使(如清單 3 中那樣)創建兩個不同的 Singleton 對象成為不可能。假設有下列事件序列:
線程 1 進入 getInstance() 方法。
- 由于 instance 為 null,線程 1 在 //1 處進入 synchronized 塊。
- 線程 1 被線程 2 預占。
- 線程 2 進入 getInstance() 方法。
- 由于 instance 仍舊為 null,線程 2 試圖獲取 //1 處的鎖。然而,由于線程 1 持有該鎖,線程 2 在 //1 處阻塞。
- 線程 2 被線程 1 預占。
- 線程 1 執行,由于在 //2 處實例仍舊為 null,線程 1 還創建一個 Singleton 對象并將其引用賦值給 instance。
- 線程 1 退出 synchronized 塊并從 getInstance() 方法返回實例。
- 線程 1 被線程 2 預占。
- 線程 2 獲取 //1 處的鎖并檢查 instance 是否為 null。
- 由于 instance 是非 null 的,并沒有創建第二個 Singleton 對象,由線程 1 創建的對象被返回。
雙重檢查鎖定背后的理論是完美的。不幸地是,現實完全不同。雙重檢查鎖定的問題是:并不能保證它會在單處理器或多處理器計算機上順利運行。
雙重檢查鎖定失敗的問題并不歸咎于 JVM 中的實現 bug,而是歸咎于 Java 平臺內存模型。內存模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。
無序寫入
為解釋該問題,需要重新考察上述清單 4 中的 //3 行。此行代碼創建了一個 Singleton 對象并初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行之前,變量 instance 可能成為非 null 的。
什么?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。假設清單 4 中代碼執行以下事件序列:
- 線程 1 進入 getInstance() 方法。
- 由于 instance 為 null,線程 1 在 //1 處進入 synchronized 塊。
- 線程 1 前進到 //3 處,但在構造函數執行之前,使實例成為非 null。
- 線程 1 被線程 2 預占。
- 線程 2 檢查實例是否為 null。因為實例不為 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton對象。
- 線程 2 被線程 1 預占。
- 線程 1 通過運行 Singleton 對象的構造函數并將引用返回給它,來完成對該對象的初始化。
此事件序列發生在線程 2 返回一個尚未執行構造函數的對象的時候。
為展示此事件的發生情況,假設為代碼行 instance =new Singleton(); 執行了下列偽代碼: instance =new Singleton();
mem = allocate(); //Allocate memory for Singleton object.
instance = mem; //Note that instance is now non-null, but
//has not been initialized.
ctorSingleton(instance); //Invoke constructor for Singleton passing
//instance.
這段偽代碼不僅是可能的,而且是一些 JIT 編譯器上真實發生的。執行的順序是顛倒的,但鑒于當前的內存模型,這也是允許發生的。JIT 編譯器的這一行為使雙重檢查鎖定的問題只不過是一次學術實踐而已。
為說明這一情況,假設有清單 5 中的代碼。它包含一個剝離版的 getInstance() 方法。我已經刪除了“雙重檢查性”以簡化我們對生成的匯編代碼(清單 6)的回顧。我們只關心 JIT 編譯器如何編譯 instance=new Singleton(); 代碼。此外,我提供了一個簡單的構造函數來明確說明匯編代碼中該構造函數的運行情況。
3. 靜態內部類實現
public class LogProcessorUtils {
private LogProcessorUtils() {
}
/**
* 保證logProcessor單例
*/
private static class ProcessorSingletonHolder {
private final static LogProcessorInterface logProcessor = new LogProcessorImpl();
}
public static String processLog2Json(String s) {
return ProcessorSingletonHolder.logProcessor.process(s);
}
}
之后,FindBugs警告消失。
文章列表
留言列表