JMM的“非完整”淺析

所謂JMM,即Java Memory Model。
所謂“非完整”,即這篇blog只是很淺顯的一些筆記,而且所呈現的知識結構體系也是零散和不完整的。原因在於個人的懶散。當我發現要完整的搞懂JMM需要通讀大概幾十頁的英文paper時,我退縮了。這實在是個喫力不討好的事情,而且我現在也沒有這麼多時間去完整的瞭解JMM。所以,以下的內容只是在瀏覽過wiki和一些書的相關章節後組織起來的。它能夠帶給你的可能只有一些感性和淺顯的認識。
如果想完整系統的瞭解JMM,這裏給出了最權威的文獻:
The Java Memory Model, Jeremy Manson, ACM Transactions on Programming Languages and Systems, Vol. TBD, No. TDB, Month Year, Pages 1–64  --  64頁的一篇journal paper
The Java Memory Model, Jeremy Manson, POPL 05 – 14頁的一篇conference paper

 

從Double-check Locking說起
但凡介紹java concurrency的文章,就沒有不提及double-check locking(DCL)的。因爲這是一個很好的例子,來展現JMM所帶來的對於編程人員的影響。
在談DCL之前,又得先介紹lazy initialization。
在很多程序中,我們常常需要做這麼一件事:某些成員只在它將要被使用的時候才初始化(initialization),如:

上面的代碼中,helper一開始是null,沒有被初始化。直到程序需要使用helper的時候(即調用getHelper()),我們才調用helper的構造函數。
這樣的代碼其實在很多singleton pattern(單例模式)中都能找到。

很明顯,上述的code在多線程環境下是不安全的(unsafe),原因很簡單,就不多說了。那麼,怎樣才能寫一個線程安全的呢:

這是一個在java中很常見的寫法。簡單的將整個函數設置爲synchronized。這意味着任何時刻只有一個線程能夠進入到這段代碼區。毫無疑問,這麼做一定是線程安全。但它有一點點小問題(有時候這個問題可能就不小了),即它的性能不是太好。因爲當helper被初始化以後,你可以理解它就是一個只讀的變量了,而對於一個只讀的變量,沒必要做任何的同步。但由於設置了synchronized,所以永遠都只能有一個線程來訪問這段代碼。效率實在是不高。

 

想點辦法再改進一下吧:

在這段代碼中,我們解決了前面提到的性能不高的問題。首先,我們先檢查一下helper是否爲null,如果爲null,那麼就進入synchronized的區域,初始化helper。但如果不爲null,那麼說明helper已經初始化好了,那麼就不需要進入synchronzied區域,直接讀取helper。這樣,當helper真正創建之後,線程再去訪問getHelper()這個函數就不需要再做任何的同步。
而且需要注意的是,當我們進入到synchronzied區域後,我們仍然會再一次的檢查helper是否爲null。因爲檢查了兩次helper,就稱爲double-check,這段代碼就是典型的DCL。
如果寫過C/C++的多線程程序的人應該都寫過類似的代碼。它在C/C++中沒有問題。
但是,這種寫法在Java中是錯誤的!
爲什麼?

 

JMM的一些概念

在現代的處理器,特別是多核的處理器架構中,採用了非常多的對指令的優化措施。這些優化會使得程序執行的順序和程序書寫的順序不一致。另外,在現代的內存架構,特別是NUMA,每個processor都有一個local cache, 而這些processor又共享同一個內存。如果有多個processor中的cache都放置了memory中的同一數據時,很難保證這些cache中的數據是同一個版本。因此,如果多個thread共享同一數據時,很多時候它們看到的都是不同的值。
正是因爲這些優化帶來的複雜性,並且不同的平臺,不同的處理器也各不相同,所以JAVA提出了它自己的Memory Model,以達到某種統一。所謂的Java Memory Model,其實就是一些規範,這些規範說明了thread在訪問memory時的一些規則以及這些thread怎樣通過memory進行交互(wiki的原話是:The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language)。

 

這裏,我很難給出這些規範的詳細定義,只是提及幾個基本的概念:
as-if-seria l: 我給出的中文直白的解釋就是“看起來像串行執行的”。這是JMM中關於單線程執行的一個規範。意思就是說,無論編譯器或者processor怎樣優化指令的執行,怎樣去打亂指令執行的順序,但是必須保證,最後的執行結果必須和按照書寫順序的程序執行的結果是一樣的。其實,也可以反過來理解,就是隻要執行結果和書寫的結果是一樣的,那麼任何的優化和亂序都是可以接受的。
但是,as-if-serial這個語義只規範了單個線程內部的執行,並沒有涉及到多個線程之間。它不能阻止多個線程會讀到同一個共享數據的不同的值。事實上,JMM也確實沒有定義說多個線程讀共享數據的值一定是一致的。相對的,JMM給出了一個非常寬泛的規範:
happens-before order: 如果B開始執行的時候A一定已經完成了(無論A,B是在同一線程還是不同線程),那麼A和B就滿足happens-before的關係。JMM定義了很多這樣的關係,比如:
  Monitor lock rule. 如果發生了lock操作,那麼它對應的unlock操作一定發生在其它的lock操作之前,如圖:

  Volatile variable rule. 一個對於volatile field的寫操作一定在它後面的讀操作之前完成;
  Thread start rule. 一個thread.start一定發生在這個線程中的每個action之前;
  當然,happens-before滿足傳遞性。就是說,如果A happens-before B,而B happens-before C,那麼A happens-before C。

 

說了半天,再來看個例子:

由於兩個thread之間沒有任何的明確的同步操作,所以不存在任何happens-before的關係。所以,可能出現多種的執行順序:

如果考慮到單線程內的代碼有可能亂序的話(這個亂序並沒有違背as-if-serial),那麼還有一種結果:

 

回到DCL

回到前面的DCL代碼,注意“helper = new Helper();”這條語句。正是由於現代處理器的各種優化手段,導致在執行這條語句的時候,在Helper的構造函數還沒有完全結束時就可能對helper賦值,所以,某些情況下,雖然getHelper()這個函數返回了一個對於Helper的引用,但有可能這個引用所指向的那個對象還沒有完全構造好,還是一個半成品。舉例來說:
Thread A第一個調用了getHelper(),這時候helper肯定是null,所以進入synchronized區域,執行Helper的構造函數;
由於處理器的優化,Helper的構造沒有完全結束,就返回了引用給helper;
這時候Thread B調用了getHelper(),由於helper已經不爲空了,所以Thread B不會進入synchronized區域,而是直接得到helper引用;
Thread B用getHelper()返回的引用來做其它的事情了,但是這個時候可能Helper的構造函數還沒有完成,所以程序出問題了;

 

再多羅嗦兩句,Thread B之所以可能得到一個不完整的Helper的引用,是因爲它並沒有進入到synchronized區域,所以它沒有受到我們前面提到的happens-before rule的保護。假設如果Thread B需要進入synchronized區域才能獲得helper的引用,那麼根據monitor lock rule,當thread B進入到synchronized區域之前,thread A一定在synchronzied區域所執行的代碼一定全部都執行完成了,這樣thread B就不可能獲得不完整的helper。

所以,DCL通過兩次檢查helper的引用來減少進入synchronzied區域的次數,能夠提高性能,但也正是因爲如此,它就面臨了一個風險,即可能獲得一個不完整的helper的引用。

 

最後,再補充一個方法,在java中能夠做到threadsafe而又性能很高的lazy initialization:

這個方法起作用的原因是,HelperHolder是一個inner class,那麼它只有在被使用的時候纔會被加載到JVM中。

 

Reference

[1] Wiki: Double-check Locking http://en.wikipedia.org/wiki/Double-checked_locking

[2] Wiki: JMM http://en.wikipedia.org/wiki/Java_Memory_Model

[3] Java concurrency in Practice

[4] The art of multiprocessor programming

 

補充一個很詳細的關於Java Double-check Locking的講解:

http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章