深刻淺出 Java Concurrency (4): 原子操做 part 3 指令重排序與happens-before法則


在這個小結裏面重點討論原子操做的原理和設計思想。html

因爲在下一個章節中會談到鎖機制,所以此小節中會適當引入鎖的概念。java

在Java Concurrency in Practice中是這樣定義線程安全的:緩存

當多個線程訪問一個類時,若是不用考慮這些線程在運行時環境下的調度和交替運行,而且不須要額外的同步及在調用方代碼沒必要作其餘的協調,這個類的行爲仍然是正確的,那麼這個類就是線程安全的。安全

顯然只有資源競爭時纔會致使線程不安全,所以無狀態對象永遠是線程安全的。架構

原子操做的描述是: 多個線程執行一個操做時,其中任何一個線程要麼徹底執行完此操做,要麼沒有執行此操做的任何步驟,那麼這個操做就是原子的。併發

枯燥的定義介紹完了,下面說更枯燥的理論知識。app

指令重排序函數

Java語言規範規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同於它在嚴格的順序化環境下的結果,那麼指令的執行順序就可能與代碼的順序不一致。這個過程經過叫作指令的重排序。指令重排序存在的意義在於:JVM可以根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的從新排序機器指令,使機器指令更符合CPU的執行特色,最大限度的發揮機器的性能。性能

程序執行最簡單的模型是按照指令出現的順序執行,這樣就與執行指令的CPU無關,最大限度的保證了指令的可移植性。這個模型的專業術語叫作順序化一致性模型可是現代計算機體系和處理器架構都不保證這一點(由於人爲的指定並不能老是保證符合CPU處理的特性)。atom

咱們來看最經典的一個案例。

package xylz.study.concurrency.atomic;

public class ReorderingDemo {

static int x = 0, y = 0, a = 0, b = 0;

public static void main(String[] args) throws Exception {

for (int i = 0; i < 100; i++) {
x=y=a=b=0;
Thread one = new Thread() {
public void run() {
a = 1;
x = b;
}
};
Thread two = new Thread() {
public void run() {
b = 1;
y = a;
}
};
one.start();
two.start();
one.join();
two.join();
System.out.println(x + " " + y);
}
}

}

 

在這個例子中one/two兩個線程修改區x,y,a,b四個變量,在執行100次的狀況下,可能獲得(0 1)或者(1 0)或者(1 1)。事實上按照JVM的規範以及CPU的特性有極可能獲得(0 0)。固然上面的代碼你們不必定能獲得(0 0),由於run()裏面的操做過於簡單,可能比啓動一個線程花費的時間還少,所以上面的例子難以出現(0,0)。可是在現代CPU和JVM上確實是存在的。因爲run()裏面的動做對於結果是無關的,所以裏面的指令可能發生指令重排序,即便是按照程序的順序執行,數據變化刷新到主存也是須要時間的。假定是按照a=1;x=b;b=1;y=a;執行的,x=0是比較正常的,雖然a=1在y=a以前執行的,可是因爲線程one執行a=1完成後尚未來得及將數據1寫回主存(這時候數據是在線程one的堆棧裏面的),線程two從主存中拿到的數據a可能仍然是0(顯然是一個過時數據,可是是有可能的),這樣就發生了數據錯誤。
在兩個線程交替執行的狀況下數據的結果就不肯定了,在機器壓力大,多核CPU併發執行的狀況下,數據的結果就更加不肯定了。

Happens-before法則

Java存儲模型有一個happens-before原則,就是若是動做B要看到動做A的執行結果(不管A/B是否在同一個線程裏面執行),那麼A/B就須要知足happens-before關係。

在介紹happens-before法則以前介紹一個概念:JMM動做(Java Memeory Model Action),Java存儲模型動做。一個動做(Action)包括:變量的讀寫、監視器加鎖和釋放鎖、線程的start()和join()。後面還會提到鎖的的。

happens-before完整規則:

(1)同一個線程中的每一個Action都happens-before於出如今其後的任何一個Action。

(2)對一個監視器的解鎖happens-before於每個後續對同一個監視器的加鎖。

(3)對volatile字段的寫入操做happens-before於每個後續的同一個字段的讀操做。

(4)Thread.start()的調用會happens-before於啓動線程裏面的動做。

(5)Thread中的全部動做都happens-before於其餘線程檢查到此線程結束或者Thread.join()中返回或者Thread.isAlive()==false。

(6)一個線程A調用另外一個另外一個線程B的interrupt()都happens-before於線程A發現B被A中斷(B拋出異常或者A檢測到B的isInterrupted()或者interrupted())。

(7)一個對象構造函數的結束happens-before與該對象的finalizer的開始

(8)若是A動做happens-before於B動做,而B動做happens-before與C動做,那麼A動做happens-before於C動做。

volatile語義

到目前爲止,咱們屢次提到volatile,可是卻仍然沒有理解volatile的語義。

volatile至關於synchronized的弱實現,也就是說volatile實現了相似synchronized的語義,卻又沒有鎖機制。它確保對volatile字段的更新以可預見的方式告知其餘的線程。

volatile包含如下語義:

(1)Java 存儲模型不會對valatile指令的操做進行重排序:這個保證對volatile變量的操做時按照指令的出現順序執行的。

(2)volatile變量不會被緩存在寄存器中(只有擁有線程可見)或者其餘對CPU不可見的地方,每次老是從主存中讀取volatile變量的結果。也就是說對於volatile變量的修改,其它線程老是可見的,而且不是使用本身線程棧內部的變量也就是在happens-before法則中,對一個valatile變量的寫操做後,其後的任何讀操做理解可見此寫操做的結果。

儘管volatile變量的特性不錯,可是volatile並不能保證線程安全的,也就是說volatile字段的操做不是原子性的,volatile變量只能保證可見性(一個線程修改後其它線程可以理解看到此變化後的結果),要想保證原子性,目前爲止只能加鎖!

volatile一般在下面的場景:

 

volatile boolean done = false;

while( ! done ){
dosomething();
}

應用volatile變量的三個原則:

(1)寫入變量不依賴此變量的值,或者只有一個線程修改此變量

(2)變量的狀態不須要與其它變量共同參與不變約束

(3)訪問變量不須要加鎖

 

這一節理論知識比較多,可是這是很面不少章節的基礎,在後面的章節中會屢次提到這些特性。

本小節中仍是沒有談到原子操做的原理和思想,在下一節中將根據上面的一些知識來介紹原子操做。

 

參考資料:

(1)Java Concurrency in Practice

(2)正確使用 Volatile 變量

相關文章
相關標籤/搜索