1. 程式人生 > >從volatile解讀ConcurrentHashMap(jdk1.6.0)無鎖讀

從volatile解讀ConcurrentHashMap(jdk1.6.0)無鎖讀

作者:綾萱

volatile常常用於修飾多執行緒共享變數,用來保證該變數的可見性。volatile的語意:某個寫執行緒對volatile變數的寫入馬上可以被後續的某個讀執行緒“看”到。

volatile保證可見性的原理:volatile是通過在編譯器生成位元組碼時,在對volatile變數進行讀寫指令序列的前後加入記憶體屏障,來禁止一些處理器重排序保證寫入一定發生在讀之前的這種happen-before關係。

簡單理解:在本次執行緒內,當讀取一個變數時,為提高存取速度,編譯器優化時有時會先把變數讀取到一個執行緒本地記憶體中;以後再取變數值時,就直接從本地記憶體中取值;當變數值在本執行緒裡改變時,會同時把變數的新值copy到本地記憶體中,以便保持一致;在某個特定的時候,將本地記憶體的更改寫到系統主記憶體中去;當變數在因別的執行緒等而改變了值,並且該變化沒有寫到系統主記憶體,本次執行緒的本地記憶體中的值不會相應改變,從而造成應用程式讀取的值和實際的變數值不一致;但是當變數被volatile

修飾後,每次更改該變數的時候會將更改結果寫到系統主記憶體中,利用多處理器的快取一致性,其他處理器會發現自己的快取行對應的記憶體地址被修改,就會將自己處理器的快取行設定為失效,並強制從系統主記憶體獲取最新的資料。這樣就能保證即使在別的執行緒中改變了該變數的值,在本執行緒中也能取到最新更改後的值。 ConcurrentHashMap之所以有較好的併發性是因為ConcurrentHashMap是無鎖讀和加鎖寫,並且利用了分段鎖(不是在所有的entry上加鎖,而是在一部分entry上加鎖)。

那ConcurrentHashMap是怎麼實現無鎖讀的呢?

這是在jdk1.6.0中的讀的實現。


    不得不讚嘆,作者深厚的功底啊,當執行讀的時候,先判斷count,count就是一個Segment(充當鎖的角色)所守護HashEntry的數量。

 
    這裡的count是被volatile修飾的。當對這段表的結構進行更改時,在退出前都會去更改count。由於volatile的語意:某個寫執行緒對volatile變數的寫入馬上可以被後續的某個讀執行緒“看”到,所以這裡對count的讀一定發生在對count寫之後,獲得是最新的count。在無鎖讀的方法中,首先去讀取這個最近的count,保證了在執行無鎖讀的時候表的結構沒有被改變。(利用了volatile變數寫讀的happen-before關係)。

   同時當把value設定為volatile
時,其他執行緒所做的改變就能馬上被當前執行緒感知。這樣就能支援多個執行緒併發讀了~ 不過我們也知道volatile並不能保證執行緒安全,它是輕量級的synchronized。 要使 volatile變數提供理想的執行緒安全,必須同時滿足下面兩個條件: ● 對變數的寫操作不依賴於當前值。 ● 該變數沒有包含在具有其他變數的不變式中。 舉例:執行緒安全計數器的自增操作,其實是由3個操作讀取-修改-寫入操作序列組成的組合操作,volatile不能保證原子性,不能保證在操作期間該變數的值不會改變。
   其實這是一種常見的volatile的利用場景——開銷較低的讀-寫鎖策略。如果讀操作遠遠超過寫操作,您可以結合使用內部鎖和 volatile變數來減少公共程式碼路徑的開銷。這樣讀操作只是volatile讀操作,效能優於一個無競爭的鎖獲取的開銷。但是當需要對該變數執行寫操作,應該加鎖。

  PS:這裡ConcurrentHashMap也有加鎖讀的情況。利用方法  V readValueUnderLock(HashEntry<K,V> e)。只有value為空的時候,才會加鎖讀,這種情況就是編譯器對value的賦值操作進行重排序了。

    感謝家純師兄的訂正和指導。