Double-checked locking真的有效嗎?
作者: zsxwing 日期: 2011-04-29 10:48:06
在很多設計模式的書籍中,我們都可以看到類似下面的單例模式的實現代碼,一般稱為Double-checked locking(DCL)
01 | public class Singleton { |
03 | private static Singleton instance; |
09 | public static Singleton getInstance() { |
10 | if (instance == null ) { //1 |
11 | synchronized (Singleton. class ) { //2 |
12 | if (instance == null ) { //3 |
13 | instance = new Singleton(); //4 |
這樣子的代碼看起來很完美,可以解決instance的延遲初始化。只是,事實往往不是如此。
問題在于instance = new Singleton();這行代碼。
在我們看來,這行代碼的意義大概是下面這樣子的
mem = allocate(); //收集內存
ctorSingleton(mem); //調用構造函數
instance = mem; //把地址傳給instance
這行代碼在Java虛擬機(JVM)看來,卻可能是下面的三個步驟(亂序執行的機制):
mem = allocate(); //收集內存
instance = mem; //把地址傳給instance
ctorSingleton(instance); //調用構造函數
下面我們來假設一個場景。
- 線程A調用getInstance函數并且執行到//4。但是線程A只執行到賦值語句,還沒有調用構造函數。此時,instance已經不是null了,但是對象還沒有初始化。
- 很不幸線程A這時正好被掛起。
- 線程B獲得執行的權力,然后也開始調用getInstance。線程B在//1發現instance已經不是null了,于是就返回對象了,但是這個對象還沒有初始化,于是對這個對象進行操作就出錯了。
問題就出在instance被提前初始化了。
解決方案一,不使用延遲加載:
01 | public class Singleton { |
03 | private static Singleton instance = new Singleton(); |
09 | public static Singleton getInstance() { |
JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,并且會保證把賦值給instance的內存初始化完畢。
解決方案二,利用一個內部類來實現延遲加載:
01 | public class Singleton { |
07 | private static class SingletonContainer { |
08 | private static Singleton instance = new Singleton(); |
11 | public static Singleton getInstance() { |
12 | return SingletonContainer.instance; |
這兩種方案都是利用了JVM的類加載機制的互斥。
方案二的延遲加載實現是因為,只有在第一次調用Singleton.getInstance()函數時,JVM才會去加載SingletonContainer,并且初始化instance。
不只Java存在這個問題,C/C++由于CPU的亂序執行機制,也同樣存在這樣的問題。
抱歉,我之前的理解有誤,DCL在Java中失效的原因是JIT比較激進的優化導致的,在C/C++并不會由于CPU的亂序執行(調用構造函數和賦值這兩個操作對CPU來說絕對不會亂序的)產生這個問題。
暫時不知道Java對于這個問題是否修復了。