您當前的位置:首頁 > 書法

ReentrantLock是如何保證記憶體的可見性的?

作者:由 wuxinliulei 發表于 書法時間:2019-09-03

部分內容摘錄自:

以及深入理解Java虛擬機器 JVM高階特性與最佳實踐 第12章

https://www。

cnblogs。com/diegodu/p/7

998337。html

先描述幾個定義:

關於主記憶體與

工作記憶體

之間具體的互動協議,即一個變數如何從主記憶體複製到工作記憶體,如何從工作記憶體同步回主記憶體之類的實現細節,java記憶體模型中定義了8種操作來完成,虛擬機器實現時必須保證這8種操作都是原子的、不可分割的(對於long和double型別的變數來說,load、store、read跟write在某些平臺上允許例外)。

8種基本操作:

lock,鎖定,所用於主記憶體變數,它把一個變數標識為一條執行緒獨佔的狀態。

unlock,解鎖,解鎖後的變數才能被其他執行緒鎖定。

read,讀取,所用於主記憶體變數,它把一個主記憶體變數的值,讀取到工作記憶體中。

load,載入,所用於

工作記憶體變數

,它把read讀取的值,放到工作記憶體的變數副本中。

use,使用,作用於工作記憶體變數,它把工作記憶體變數的值傳遞給執行引擎,當JVM遇到一個變數讀取指令就會執行這個操作。

assign,賦值,作用於工作記憶體變數,它把一個從執行引擎接收到的值賦值給工作記憶體變數。

store,儲存,作用域工作記憶體變數,它把工作記憶體變數值傳送到主記憶體中。

write,寫入,作用於主記憶體變數,它把store從工作記憶體中得到的

變數值

寫入到主記憶體變數中

8種操作的規則:

java記憶體模型還規定了在執行上述8種基本操作時必須滿足如下規則:

不允許read和load、store和write操作之一單獨出現,即不允許載入或同步工作到一半。

不允許一個執行緒丟棄它最近的assign操作,即變數在工作記憶體中改變了之後,必須吧改變化同步回主記憶體。

不允許一個執行緒無原因地(無assign操作)把資料從工作記憶體同步到主記憶體中。

一個新的變數只能在主記憶體中誕生。

一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,,多次lock之後必須要執行相同次數的unlock操作,變數才會解鎖。

如果對一個物件進行lock操作,那會清空工作記憶體變數中的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。

如果一個變數事先沒有被lock,就不允許對它進行unlock操作,也不允許去unlock一個被其他執行緒鎖住的變數。

對一個變數執行unlock操作之前,必須將此變數同步回主記憶體中(執行store、write)。

有如上8種記憶體訪問操作以及規則限定,再加上對volatile的一些特殊規定,就已經完全確定了java程式中哪些記憶體訪問操作是在併發下安全的。

對於volatile的特殊規則:

volatile有兩個特性:1、對所有執行緒可見;2、防止指令重排;我們接下來說明一下這兩個特性。

可見性,是指當一條執行緒修改了某個volatile變數的值,新值對於其它執行緒來說是可以立即知道的。而普通變數無法做到這點。但這裡有個誤區,由於volatile對所有執行緒立即可見,對volatile的寫操作會立即反應到其它執行緒,因此基於volatile的變數的運算在併發下是安全的。這是錯誤的,原因是volatile所謂的其它執行緒立即知道,是其它執行緒在使用的時候會讀讀記憶體然後load到自己工作記憶體,如果這時候其它執行緒進行了修改,本執行緒的volatile變數狀態會被置為無效,會重新讀取,但如果本執行緒的變數已經被讀入執行棧幀,那麼是不會重新讀取的;那麼兩個執行緒都把本地工作記憶體內容寫入主存的時候就會發生覆蓋問題,導致併發錯誤。

防止指令重排,重排序最佳化是機器級的操作,也就是硬體級別的操作。重排序會打亂程式碼順序執行,但會保證在執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,因此在一個執行緒的方法執行過程中無法感知到重排的操作影響,這也是“執行緒內表現為序列”的由來。volatile的遮蔽重排序在jdk1。5後才被修復。原理是volatile生成的彙編程式碼多了一條帶lock字首的空操作的命令,而根據IA32手冊規定,這個lock字首會使得本cpu的快取寫入記憶體,而寫入動作也會引起別的cpu或者別的核心無效化,這相當於對cpu快取中的變數做了一次store跟write的操作,所以透過這樣一個操作,可以讓變數對其它cpu立即可見(因為狀態被置為無效,用的話必須重新讀取)。

另外,java記憶體模型對

volatile變數

有三條特殊規則:

a、每次使用變數之前都必須先從主記憶體重新整理最新的值,用於保證能看見其它執行緒對變數的修改;

b、每次對變數修改後都必須立刻同步到主記憶體中,用於保證其它執行緒可以看到自己的修改;

c、兩個變數都是volatile的,將資料同步到記憶體的時候,先讀的先寫;

1、原子性(Atomicity)

原子性是指在一個操作中就是cpu不可以在中途暫停然後再排程,既不被中斷操作,要不執行完成,要不就不執行。

如果一個操作時原子性的,那麼多執行緒併發的情況下,就不會出現變數被修改的情況

比如 a=0;(a非long和double型別) 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。

非原子操作都會存線上程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以透過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

(由Java記憶體模型來直接保證的

原子性變數

操作包括read、load、use、assign、store和write六個,大致可以認為基礎資料型別的訪問和讀寫是具備原子性的。如果應用場景需要一個更大範圍的原子性保證,Java記憶體模型還提供了lock和unlock操作來滿足這種需求,儘管虛擬機器未把lock與unlock操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter和monitorexit來隱匿地使用這兩個操作,這兩個位元組碼指令反映到Java程式碼中就是同步塊—synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。)

2、可見性(Visibility)

可見性就是指當一個執行緒修改了執行緒共享變數的值,其它執行緒能夠立即得知這個修改。Java

記憶體模型

是透過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方法來實現可見性的,無論是普通變數還是volatile變數都是如此,普通變數與volatile變數的區別是volatile的特殊規則保證了新值能立即同步到主記憶體,以及每使用前立即從記憶體重新整理。因為我們可以說volatile保證了執行緒操作時變數的可見性,而普通變數則不能保證這一點。

除了volatile之外,Java還有兩個關鍵字能實現可見性,它們是synchronized。同步塊的可見性是由“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store和write操作)”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的欄位是構造器一旦初始化完成,並且構造器沒有把“this”引用傳遞出去,那麼在其它執行緒中就能看見final欄位的值。

(可見性,是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改的結果。另一個執行緒馬上就能看到。比如:用volatile修飾的變數,就會具有可見性。volatile修飾的變數不允許

執行緒內部快取

和重排序,即直接修改記憶體。所以對其他執行緒是可見的。但是這裡需要注意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變數a具有可見性,但是a++ 依然是一個非原子操作,也就這這個操作同樣存線上程安全問題。)

3、

有序性

(Ordering)

Java記憶體模型中的程式天然有序性可以總結為一句話:如果在本執行緒內觀察,所有操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。前半句是指“執行緒內表現為

序列語義

”,後半句是指“指令重排序”現象和“工作記憶體中主記憶體同步延遲”現象。

Java語言提供了volatile和synchronized兩個關鍵字來保證執行緒之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變數在同一時刻只允許一條執行緒對其進行lock操作”這條規則來獲得的,這個規則決定了持有同一個鎖的兩個同步塊只能序列地進入。

先行發生原則:

如果Java記憶體模型中所有的有序性都只靠volatile和synchronized來完成,那麼有一些操作將會變得很囉嗦,但是我們在編寫Java併發程式碼的時候並沒有感覺到這一點,這是因為Java語言中有一個“先行發生”(Happen-Before)的原則。這個原則非常重要,它是判斷資料是否存在競爭,執行緒是否安全的主要依賴。

先行發生原則是指Java記憶體模型中定義的兩項操作之間的依序關係,如果說操作A先行發生於操作B,其實就是說發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包含了修改了記憶體中共享變數的值、傳送了訊息、呼叫了方法等。它意味著什麼呢?如下例:

//執行緒A中執行

i = 1;

//執行緒B中執行

j = i;

//執行緒C中執行

i = 2;

假設執行緒A中的操作”i=1“先行發生於執行緒B的操作”j=i“,那麼我們就可以確定線上程B的操作執行後,變數j的值一定是等於1,結出這個結論的依據有兩個,一是根據先行發生原則,”i=1“的結果可以被觀察到;二是執行緒C登場之前,執行緒A操作結束之後沒有其它執行緒會修改變數i的值。現在再來考慮執行緒C,我們依然保持執行緒A和B之間的先行發生關係,而執行緒C出現線上程A和B操作之間,但是C與B沒有先行發生關係,那麼j的值可能是1,也可能是2,因為執行緒C對應變數i的影響可能會被執行緒B觀察到,也可能觀察不到,這時執行緒B就存在讀取到過期資料的風險,不具備多執行緒的安全性。

下面是Java記憶體模型下一些”天然的“先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推匯出來的話,它們就沒有順序性保障,虛擬機器可以對它們進行隨意地重排序。

a。

程式次序規則

(Pragram Order Rule):在一個執行緒內,按照程式程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是程式程式碼順序,因為要考慮分支、迴圈結構。

b。管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裡必須強調的是同一個鎖,而”後面“是指時間上的先後順序。

c。volatile變數規則(Volatile Variable Rule):對一個volatile變數的寫操作先行發生於後面對這個變數的讀取操作,這裡的”後面“同樣指時間上的先後順序。

d。執行緒啟動規則(Thread Start Rule):Thread物件的start()方法先行發生於此執行緒的每一個動作。

e。

執行緒終於規則

(Thread Termination Rule):執行緒中的所有操作都先行發生於對此執行緒的終止檢測,我們可以透過Thread。join()方法結束,Thread。isAlive()的返回值等作段檢測到執行緒已經終止執行。

f。執行緒中斷規則(Thread Interruption Rule):對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以透過Thread。interrupted()方法檢測是否有中斷髮生。

g。物件終結規則(Finalizer Rule):一個物件初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。

g。

傳遞性

(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

一個操作”時間上的先發生“不代表這個操作會是”先行發生“,那如果一個操作”先行發生“是否就能推匯出這個操作必定是”時間上的先發生“呢?也是不成立的,一個典型的例子就是指令重排序。所以時間上的先後順序與先生髮生原則之間基本沒有什麼關係,所以衡量併發安全問題一切必須以先行發生原則為準。

那問題來了,ReentrantLock能保證有序性,可見性是如何實現的呢?

ReentrantLock可見性保證的具體實現是什麼?

j。u。c。locks。Lock介面定義了六個方法:

public interface Lock {

void lock();

void lockInterruptibly() throws InterruptedException;

boolean tryLock();

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

void unlock();

Condition newCondition();

}

在j。u。c包中實現Lock介面的類主要有ReentrantLock和ReentrantReadWriteLock,下面以ReentrantLock為例來說明(ReentrantReadWriteLock原理相同)。

先來看ReentrantLock類的lock方法和unlock方法的實現:

public void lock() {

sync。lock();

}

// sync。lock()實現

final void lock() {

acquire(1);

}

public void unlock() {

sync。release(1);

}

lock方法和unlock方法的具體實現都代理給了sync物件,來看一下sync物件的定義:

abstract static class Sync extends AbstractQueuedSynchronizer

static final class FairSync extends Sync

public ReentrantLock(boolean fair) {

sync = fair ?

new FairSync

() : new NonfairSync();

}

根據ReentrantLock的構造引數,sync物件可以是FairSync(公平鎖)或者是NonfairSync(非公平鎖),我們以FairSync為例(NonfairSync原理類似)來說明。

從上面程式碼中可以看出,lock方法和unlock方法的具體實現都是由acquire和release方法完成的,而FairSync類中並沒有定義acquire方法和release方法,這兩個方法都是在Sync的父類AbstractQueuedSynchronizer類中實現的。

public final void acquire(int arg) {

// 只關注tryAcquire即可

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node。EXCLUSIVE), arg))

selfInterrupt();

}

public final boolean release(int arg) {

// 只關注tryRelease即可

if (tryRelease(arg)) {

Node h = head;

if (h != null && h。waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

acquire方法的大致步驟:tryAcquire會嘗試獲取鎖,如果獲取失敗會將當前執行緒加入等待佇列,並掛起當前執行緒。當前執行緒會等待被喚醒,被喚醒後再次嘗試獲取鎖。

release方法的大致步驟:tryRelease會嘗試釋放鎖,如果釋放成功可能會喚醒其它執行緒,釋放失敗會丟擲異常。

我們可以看出,獲取鎖和釋放鎖的具體操作是在tryAcquire和tryRelease中實現的,而tryAcquire和tryRelease在父類AbstractQueuedSynchronizer中沒有定義,留給子類FairSync去實現。

我們來看一下FairSync類的tryAcquire和tryRelease的具體實現:

//

state變數

定義在AbstractQueuedSynchronizer中,表示同步狀態。

private volatile int state;

protected final boolean tryAcquire(int acquires) {

final Thread current = Thread。currentThread();

// 讀State

int c = getState();

if (c == 0) {

// 獲取到鎖會寫state

if (!hasQueuedPredecessors() &&

compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

if (nextc < 0)

throw new Error(“Maximum lock count exceeded”);

// 寫state

setState(nextc);

return true;

}

return false;

}

protected final boolean tryRelease(int releases) {

// 讀state

int c = getState() - releases;

if (Thread。currentThread() != getExclusiveOwnerThread())

throw new IllegalMonitorStateException();

boolean free = false;

if (c == 0) {

free = true;

setExclusiveOwnerThread(null);

}

// 寫state

setState(c);

return free;

}

從上面的程式碼中可以看到有一個volatile state變數,這個變數用來表示同步狀態,獲取鎖時會先讀取state的值,獲取成功後會把值從0修改為1。當釋放鎖時,也會先讀取state的值然後進行修改。也就是說,無論是成功獲取到鎖還是成功釋放掉鎖,都會先讀取state變數的值,再進行修改。

我們將上面的程式碼做個簡化,只留下關鍵步驟:

private volatile int state;

void lock() {

read state

if (can get lock)

write state

}

void unlock() {

write state

}

假設執行緒a透過呼叫lock方法獲取到鎖,此時執行緒b也呼叫了lock方法,因為a尚未釋放鎖,b只能等待。a在獲取鎖的過程中會先讀state,再寫state。當a釋放掉鎖並喚醒b,b會嘗試獲取鎖,也會先讀state,再寫state。

我們注意到上述提到的

Happens-before規則

的第二條:

一個volatile變數的寫操作發生在這個volatile變數隨後的讀操作之前

可以推測出,當執行緒b執行獲取鎖操作,讀取了state變數的值後,執行緒a在寫入state變數之前的任何操作結果對執行緒b都是可見的。

由此,我們可以得出結論Lock介面的實現類能實現和synchronized內建鎖一樣的記憶體資料可見性。

結束語

ReentrantLock及其它Lock介面實現類實現記憶體資料可見性的方式相對比較隱秘,藉助了volatile關鍵字間接地實現了可見性。其實不光是Lock介面實現類,因為j。u。c包中大部分同步器的實現都是基於AbstractQueuedSynchronizer類來實現的,因此這些同步器也能夠提供一定的可見性,有興趣的同學可以嘗試用類似的思路去分析。