java多線程讀寫鎖ReentrantReadWriteLock源碼分析

在多線程編程中,Synchronized 和 volatile 都扮演者重要的角色,前面的文章我們已經瞭解了java內置鎖Synchronized ,它保證了併發過程中的可見性與原子性,避免了共享數據的錯誤。 而 Volatile可以看做是輕量級的 Synchronized,它只保證了共享變量的可見性。在線程 A 修改了被 volatile 修飾的共享變量之後,線程 B 能夠讀取到正確的值。在 關於JMM 的文章中我們瞭解到 java 在多線程中操作共享變量的過程中,會存在指令重排序與共享變量工作內存緩存的問題。

volatile作為一個修飾符,使用很簡單,但是它背後做了多少工作呢?

首先我們需要明白,本地內存是一個抽象概念,包括緩存、讀寫緩衝區、寄存器,甚至編譯器重排序和cpu重排序。JVM按照JMM規範對volatile進行特殊處理,從而實現在CPU對該變量的特殊處理。

volatile底層原理

計算機系統中,硬盤負責存儲數據, 但是數據交換速度慢,CPU 運行速度非常快,CPU直接硬盤的數據交換效率非常低,於是產生了內存,通過內存與 CPU 進行數據交換,但是內存的速度依舊不夠快,嚴重拖慢整體的運行效率,故而在 CPU 內部添加了高速緩存,作為 CPU的臨時存儲器,與內存的數據交互。

  • 在單核CPU中,多線程都在一個CPU中進行運行,共用一份緩存,對同一個共享變量的使用,而不會出現數據可見性的問題
  • 而多核CPU由於多線程可能分配在不同的CPU,這種情況下進行計算時,就會出現一個CPU內核計算完成,並沒有同步回主內存,而其他CPU無法使用最新的數據,而出現了可見性問題。

通過添加volatile修飾,通過JVM的優化,最後反應到CPU上,先從內存獲取數據,存儲在高速緩存中,然後再從高速緩存中獲取數據進行計算,計算完成後的值並不會立即刷新回主內存中,而其他 CPU 這時並不知道變量值已經改變,使用的還是之前的變量值進行計算,這就產生了數據錯誤。這種機制類似我們之前講過的 JMM 中主內存於工作內存的關係。

我們知道,javac 編譯器將 .java 代碼編譯成為 .class 字節碼,JVM 通過解釋器與即時編譯器(JIT)運行字節碼中的指令,將字節碼指令翻譯稱為具體的機器碼指令,而被 volatile 修飾的共享變量,在翻譯成為機器碼的過程中為其賦值操作添加特殊機器碼指令前綴Lock xxxx。

public class Test{
private volatile int i=1;//被 volatile 修飾

//線程A修改
public void setVar(){
i=2;
}

//線程B獲取
public int getVar(){
return i;
}
}

在執行此條指令時,Lock 指令有兩個作用:

  • 使本CPU的緩存寫入內存
  • 上面的寫入動作也會引起別的CPU或別的內核中的緩存無效,

所以通過這樣一個指令前綴,可以讓對volatile變量的修改對其他CPU可見。

指令重排序

還是上文中的Lock前綴的作用,為什麼它能禁止指令重排序呢?

從JMM角度講:

在JMM的邏輯實現中,當操作一個變量 執行putfield指令(為變量賦值) 時,JVM會檢查此變量是否是被volatile修飾的,如果是的話,JVM會為該變量添加內存屏障,用於隔離該變量與前後操作,從而禁止volatile變量的操作與前後操作的亂序。

摘自java併發編程的藝術: 為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來 禁止特定類型的處理器重排序。對於編譯器來說,發現一個最優佈置來最小化插入屏障的總 數幾乎不可能。為此,JMM採取保守策略。下面是基於保守策略的JMM內存屏障插入策略。 ·在每個volatile寫操作的前面插入一個StoreStore屏障。 ·在每個volatile寫操作的後面插入一個StoreLoad屏障。 ·在每個volatile讀操作的後面插入一個LoadLoad屏障。 ·在每個volatile讀操作的後面插入一個LoadStore屏障。

java多線程讀寫鎖ReentrantReadWriteLock源碼分析

從CPU執行角度講:

以上的內存屏障就會在執行時生成相應帶有Lock前綴的機器碼(全面已提及)。在CPU中,程序的執行計算是由CPU在不影響邏輯結果的前提下分配給不同的電路去處理邏輯,Lock指令前綴刷新回內存,必然是在此指令之前的運算全部計算完成之後,取得正確的結果才會刷新回內存的,所以這也形成了一道內存屏障,表示對該變量操作之前的操作不會亂序到其後,其後的操作不會亂序到之前。

綜上述,volatile的實現就是一個Lock指令前綴的作用。

使用注意事項

volatile雖然保證了可見性,但是它不保證原子性。

諸如i++之類的語句,在執行時的步驟:

  1. 從內存取值,放到CPU緩存中
  2. CPU中i+1
  3. 存在緩存中
  4. 刷新會內存

可見這這並不是單純的賦值操作,而是有在第4步完成之前,其他CPU內核是看不到數值變化的,而如果僅用volatile修飾的話,僅僅保證了第3部完成之後,會立即刷新回內存,但不會保證第2步計算與第3,4步的原子性。如果線程A計算+1之後,沒有刷回內存,線程B也+1,那麼最後的結果肯定是比期望的結果小的。所以在多線程操作++時,還是應該使用synchronized等同步操作保證原子性。

volatile比synchronized輕量,只保證可見性。正因如此,在java.util.concurrent中AQS使用了被volatile修飾的變量來標記狀態,實現了靈活多樣的各種鎖,補充了內置鎖synchronized的互斥等缺點。


分享到:


相關文章: