閱讀350 返回首頁    go 阿裏雲 go 技術社區[雲棲]


從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的賦值操作進行重排序了。

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

最後更新:2017-05-22 14:32:45

  上一篇:go  理解JAVA的傳值方式
  下一篇:go  Inxi:一個功能強大的獲取 Linux 係統信息的命令行工具