關於雙重檢查加鎖(DCL)的理解
雙重檢查加鎖的一般形式:
class SomeClass { private Resource resource = null; public Resource getResource() { if (resource == null) { synchronized { if (resource == null) resource = new Resource(); } } return resource; } }
援引java併發程式設計實戰的解釋:
java併發程式設計實戰在這個地方解釋的有點亂,從這個章節蹦到別的章節。。。
確實,正如她所說,問題的關鍵在於,執行緒可能看到一個僅被部分構造的Resource;具體解釋如下:
1.首先想想前面給的Happens-Before原則,
此原則用於檢測某操作是否能進行重排序,而重排序是編譯器對JVM位元組碼進行重排序的操作在這裡關於“操作”兩個字定義的很模糊(相當於沒有定義),那什麼是操作呢,什麼又是一個不可再分的原子操作呢;對JVM層面來說,一個JVM位元組碼就是一個對於JVM執行指令不能再分的原子操作,且上下位元組碼的操作結果完全互相可見,再往下可能會有更細粒度的位元組碼以及機器指令,但是現在只討論JVM位元組碼;在java語言中,對基本變數int char等的讀寫是一個原子操作,編譯成位元組碼只有一條(這才是原因),因此很多書上經常寫關於基本變數的讀寫操作來作為例子,很久之前我還以為一條java程式碼是一個原子操作。。。
在這裡定義的操作的含義就是一條java程式碼(有時候還需要根據具體語境具體分析其到底指的是什麼),對於物件型別的new
可以看到上述僅僅一個new Object()物件都有好幾條JVM位元組碼與之對應,更別說一個複雜耗時的大物件的new或者初始化了,上面講到,當兩個操作不滿足Happens-before時,JVM將對其進行重排序,對於最上面的那個DCL例子而言,執行緒A最先進來發現resource為null,因此進入同步塊,再次觀測resource==null,此時進行初始化,接下來的操作JVM可能會對其進行重排序,
我們不妨假設是這樣一種執行順序,1.為物件分配記憶體 2.呼叫建構函式 3.初始化成員變數
再假設當操作1完成時,發生了執行緒切換(這個例子很極端,一般不會只執行一條指令就會發生切換,在此只為說明情況而設),執行緒B進入了此方法,他首先觀察resource,發現resource==null為false,為什麼呢?想想==含義,其內部是判斷物件地址是不是null,此時已經為其分配了記憶體,有了地址,此時將會resource==null判斷為true了,接著執行緒B將會返回一個尚未被構造完成的resource引用,此時DCL broken....
接著java併發程式設計實戰中說將resource域宣告為volatile的,私以為這樣做也是不怎麼可取的,因為volatile這個東西只是說對讀寫操作的前後可見性,但是沒說對volatile宣告的物件其成員屬性也是這樣的,除非你宣告的volatile物件其內部所有屬性都是volatile的,你可以一試。而且這個關鍵字很多部落格說是依賴於JVM的,所以不要對其做出過多假設(雖然大多數人都是一個JVM平臺的);畢竟這本書也給了相應的改進措施-延長初始化佔位類模式,為什麼不用呢///
對本文有幫助的帖子,比較老了,不確定是否適用當前版本(某種編譯器對DCL做了某種優化或者JVM底層實現改變都說不定),但意義上是對的
-- 分 割 線 --
更正:
上面說,一條JVM指令對JVM來說是一個原子操作這句話是不嚴謹的,根據《深入瞭解JVM》這本書,即時編譯出來一條位元組碼指令,也並不意味著這條指令就是一個原子操作,一條位元組碼指令在解釋執行時,直譯器將要執行許多程式碼才能實現它的語義,如果是編譯執行,一條位元組碼指令也可能轉化成若干條本地機器碼指令,但是此處使用位元組碼(某個操作都能分成好幾個位元組碼指令,一定是非原子操作)已經能說明問題。
如有問題或者部不對的地方歡迎指正,本人QQ 2900250200