從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