看 "java併發編程的藝術" 第3.8
java
在Java多線程程序中,有時候須要採用延遲初始化來下降初始化類和建立對象的開銷。雙
重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法。本文將分析雙重檢查鎖定的
錯誤根源,以及兩種線程安全的延遲初始化方案。程序員
須要注意的是, 雙重檢查鎖定自己是錯誤的!編程
在Java程序中,有時候可能須要推遲一些高開銷的對象初始化操做,而且只有在使用這些
對象時才進行初始化。此時,程序員可能會採用延遲初始化。但要正確實現線程安全的延遲初
始化須要一些技巧,不然很容易出現問題。好比,下面是非線程安全的延遲初始化對象的示例
代碼。安全
public class UnsafeLazyInitialization { private static Instance instance; public static Instance getInstance() { if (instance == null) { // 1:A線程執行 instance = new Instance(); // 2:B線程執行 } return instance; } }
在UnsafeLazyInitialization類中,假設A線程執行代碼1的同時,B線程執行代碼2。此時,線
程A可能會看到instance引用的對象尚未完成初始化(出現這種狀況的緣由見3.8.2節)。
對於UnsafeLazyInitialization類,咱們能夠對getInstance()方法作同步處理來實現線程安全
的延遲初始化。示例代碼以下。多線程
public class SafeLazyInitialization { private static Instance instance; public synchronized static Instance getInstance() { if (instance == null) { instance = new Instance(); } return instance; } }
因爲對getInstance()方法作了同步處理,synchronized將致使性能開銷。若是getInstance()方
法被多個線程頻繁的調用,將會致使程序執行性能的降低。反之,若是getInstance()方法不會被
多個線程頻繁的調用,那麼這個延遲初始化方案將能提供使人滿意的性能。
在早期的JVM中,synchronized(甚至是無競爭的synchronized)存在巨大的性能開銷。所以,
人們想出了一個「聰明」的技巧:雙重檢查鎖定(Double-Checked Locking)。人們想經過雙重檢查
鎖定來下降同步的開銷。下面是使用雙重檢查鎖定來實現延遲初始化的示例代碼。併發
public class DoubleCheckedLocking { // 1 private static Instance instance; // 2 public static Instance getInstance() { // 3 if (instance == null) { // 4:第一次檢查 synchronized (DoubleCheckedLocking.class) { // 5:加鎖 if (instance == null) { // 6:第二次檢查 instance = new Instance(); // 7:問題的根源出在這裏 } } // 8 } // 9 return instance; // 10 } // 11 }
如上面代碼所示,若是第一次檢查instance不爲null,那麼就不須要執行下面的加鎖和初始
化操做。所以,能夠大幅下降synchronized帶來的性能開銷。上面代碼表面上看起來,彷佛兩全
其美。
多個線程試圖在同一時間建立對象時,會經過加鎖來保證只有一個線程能建立對象。
·在對象建立好以後,執行getInstance()方法將不須要獲取鎖,直接返回已建立好的對象。
雙重檢查鎖定看起來彷佛很完美,但這是一個錯誤的優化!在線程執行到第4行,代碼讀
取到instance不爲null時,instance引用的對象有可能尚未完成初始化。性能
前面的雙重檢查鎖定示例代碼的第7行(instance=new Singleton();)建立了一個對象。這一
行代碼能夠分解爲以下的3行僞代碼。優化
memory = allocate(); // 1:分配對象的內存空間 ctorInstance(memory); // 2:初始化對象 instance = memory; // 3:設置instance指向剛分配的內存地址
上面3行僞代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實
發生的,詳情見參考文獻1的「Out-of-order writes」部分)。2和3之間重排序以後的執行時序如
下。spa
memory = allocate(); // 1:分配對象的內存空間 instance = memory; // 3:設置instance指向剛分配的內存地址 // 注意,此時對象尚未被初始化! ctorInstance(memory); // 2:初始化對象
上面3行僞代碼的2和3之間雖然被重排序了,但這個重排序
並不會違反intra-thread semantics。這個重排序在沒有改變單線程程序執行結果的前提下,能夠
提升程序的執行性能。
爲了更好地理解intra-thread semantics,請看如圖3-37所示的示意圖(假設一個線程A在構
造對象後,當即訪問這個對象)。
如圖3-37所示,只要保證2排在4的前面,即便2和3之間重排序了,也不會違反intra-thread
semantics。
下面,再讓咱們查看多線程併發執行的狀況。如圖線程