在文章的開始,我們先來看一段代碼以及他的執行情況:
public class PossibleRecording{
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
y = b;
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
x = a;
}
});
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
System.out.println("( " + x + " , " + y + " )");
}
}
執行結果:
( 0 , 1 )
( 1 , 0 )
( 1 , 1 )
( 0 , 0 )
對于上面這一段及其簡單的代碼,可以很簡單的想到程式是如何列印( 0 , 1 ) 或 ( 1 , 0 ) 或 ( 1 , 1 ) 的,執行緒One可以在執行緒Two開始前完成,執行緒Two也可以在執行緒One開始前完成,又或者他們可以交替完成,但是奇怪的是,程式竟然可以列印( 0 , 0 ),下圖展示了一種列印(0 , 0)的可能(由于每個執行緒中的動作都沒有依賴其他執行緒的資料流,因此這些動作可以亂序執行):

在執行程式時,為了提高性能,編譯器和處理器會對指令做重排序,記憶體級的重排序會讓程式的行為變得不可預期,而同步就抑制了編譯器、運行時和硬體對存盤操作的各種方式的重排序,否則這些重排序將會破壞JMM提供的可見性保證,JMM確保在不同的編譯器和不同的處理器平臺上,通過插入特定型別的Memory Barrier來禁止特定型別的編譯器重排序和處理器重排序,為上層提供一致的可見性保證,
那么在正確使用同步、鎖的情況下,執行緒One修改了變數a的值何時對執行緒Two可見呢?我們無法就所有場景來規定某個執行緒修改的變數何時對其他執行緒可見,但是我們可以指定某些規則,這個規則就是happens-before,從JDK 5開始,JMM就是用happens-before的概念來闡述多執行緒之間的記憶體可見性,
happens-before原則的定義如下:
- 如果一個操作 happens-before 于另一個操作,那么第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前,
- 兩個操作之間存在 happens-before 的關系,并不意味著一定要按照happens-before原則制定的順序來執行,如果重排序之后的執行結果于按照 happens-before 關系來執行的結果一致,那么這種重排序并不非法,
happens-before法則包括:
- 程式次序法則:執行緒中的每個動作A都 happens-before 于該執行緒中的每一個動作B,其中在程式中,所有的動作B都出現在動作A之后
- 監視器鎖法則:對一個監視器鎖的解鎖 happens-before 于每一個后續對同一監視器鎖的枷鎖
- volatile變數法則: 對 volatile 域的寫入操作 happens-before 于每一個后續對同一個域的讀操作
- 執行緒啟動法則: 在一個執行緒里,對Thread.start() 方法的呼叫會 happens-before 于每一個啟動執行緒中的動作
- 執行緒終結法則: 縣城中的任何動作都 happens-before 于其他執行緒檢測到這個執行緒已經終結、或者動Thread.join()的呼叫中成功回傳,或者Thread.isAlive()回傳false
- 中斷法則:一個執行緒呼叫另一個執行緒的 interrupt happens-before與被中斷的執行緒發現中斷(通過拋出InterruptedException例外,或者呼叫isInterrupted和 interrupted)
- 終結法則: 一個物件的建構式的結束 happens-before 于這個物件 finalizer 的開始
- 傳遞性: 如果A happens-before 于B,且B happens-before 于C,則A happens-before 于 C
當一個變數被多個執行緒讀取,且至少被一個執行緒寫入時,如果讀寫操作并未依照排序,就會產生資料競爭,一個正確同步的執行緒是沒有資料競爭的程式,加鎖、解鎖、對volatile變數的讀寫、啟動一個執行緒以及檢測執行緒是否結束這樣的操作均是同步動作,
FutureTask原始碼解讀
接下來看看FutureTask中是如何巧妙運用happens-before法則的,

在FutureTask中最重要的變數就是上圖中標記出來的兩個,
- state:是一個volatile修飾的變數,用于表示當前task的狀態
- outcome:用于get()回傳的正常結果,也可能是例外
注意看outcome后面的注釋,在jdk原始碼中很少有這樣的注釋,一旦有這樣的注釋,那肯定是非常重要的,
理論上講,outcome會被多個執行緒訪問,其中應該是一個執行緒可以讀寫,其他的執行緒都只能讀,那這種情況下,為啥不加上volatile呢?加上volatile的好處就是可以讓outcome和state變數被修改后,其他執行緒可以立刻感知到,但作者為啥不加上volatile呢?
在整個類中,與outcome變數的寫入操作,只有這兩個地方:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
與outcome有關的讀取操作,即get操作:
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}
接下來我們把目光集中到這三個方法上:set(),get(),report()
我們把get()和report()合并到一起,將多余的代碼去掉,如下:
public V get() {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
Object x = outcome;
if (s == NORMAL);
return (V)x;
}
從上面可以看出,當state為NORMAL的時候,回傳outcome,
再來看看set()方法:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
第二行,通過UNSAFE的cas操作將狀態從NEW狀態改為COMPLETING,cas設定成功之后,進入if方法里面,然后給outcome設定值,第四行,將state的狀態設定為NORMAL狀態,從備注中可以看到這是一個最終狀態,那從NEW狀態到NORMAL狀態,中間有一個稍縱即逝的狀態-COMPLETING,從get方法中可以看到,如果state的狀態小于等于COMPLETING(即為NEW狀態)時,就是當前執行緒沒有搶到CPU的執行時間,進入等到狀態,
我們把get()和set()的偽代碼放在一起:

首先你讀到標號為4的地方,讀到的值是NORMAL,那么說明標號為3的地方一定已經執行過了,因為state是volatile修飾過的,根據happens-before關系:volatile變數法則:對 volatile 域的寫入操作 happens-before 于每一個后續對同一個域的讀操作,所以我們可以得出標號3的代碼先于標號4的代碼執行,
而又根據程式次序規則,即:
在一個執行緒內,按照控制流順序,書寫在前面的操作先行于書寫在后面的操作,注意,這里說的是控制流順序而不是程式代碼順序,因為要考慮分支、回圈等結構,
可以得出:2 happens-before 3 happens-before 4 happens-before 5;
又根據傳遞性的規則,即:
傳遞性: 如果A happens-before 于B,且B happens-before 于C,則A happens-before 于 C
可以得出,2 happens-before 5,而2就是對outcome變數的寫入,5是對outcome變數的讀取,所以,雖然outcome的變數沒有加volatile,但是他是通過被volatile修飾的state變數,借助了變數的happens-before關系,完成了同步的操作(即寫入先于讀取),
參考文章:
推薦一個公眾號:【why技術】 https://mp.weixin.qq.com/s/1SjOChRD0a241UCsBEAfCA
https://www.cmsblogs.com/?p=2102
https://blog.csdn.net/xixi_haha123/article/details/81155796
本文著作權歸Charon和博客園共有,原創文章,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/415901.html
標籤:Java
