1. 程式人生 > >JAVA 執行緒安全三要素

JAVA 執行緒安全三要素

(轉載)

1 . 原子性(Atomicity): Java中,對基本資料型別的讀取和賦值操作是原子性操作,所謂原子性操作就是指這些操作是不可中斷的,要做一定做完,要麼就沒有執行。

比如:
i = 2; //原子
j = i; //非原子 (1 讀i,2 賦值j)
i++;   //非原子 (1 讀i,2 +1 ,3 賦值i
i = i + 1//非原子 (1 讀i,2 +1 ,3 賦值i

上面4個操作中,i=2是讀取操作,必定是原子性操作,j=i你以為是原子性操作,其實吧,分為兩步,一是讀取i的值,然後再賦值給j,這就是2步操作了,稱不上原子操作,i++i = i + 1其實是等效的,讀取i的值,加1,再寫回主存,那就是3步操作了。所以上面的舉例中,最後的值可能出現多種情況,就是因為滿足不了原子性。

這麼說來,只有簡單的讀取,賦值是原子操作,還只能是用數字賦值,用變數的話還多了一步讀取變數值的操作。有個例外是,虛擬機器規範中允許對64位資料型別(long和double),分為2次32為的操作來處理,但是最新JDK實現還是實現了原子操作的。

JMM只實現了基本的原子性,像上面i++那樣的操作,必須藉助於synchronizedLock來保證整塊程式碼的原子性了。執行緒在釋放鎖之前,必然會把i的值刷回到主存的。

2 . 可見性(Visibility):

說到可見性,Java就是利用volatile來提供可見性的。
當一個變數被volatile修飾時,那麼對它的修改會立刻重新整理到主存,當其它執行緒需要讀取該變數時,會去記憶體中讀取新值。而普通變數則不能保證這一點。

其實通過synchronized和Lock也能夠保證可見性,執行緒在釋放鎖之前,會把共享變數值都刷回主存,但是synchronized和Lock的開銷都更大。

3 . 有序性(Ordering)

lock/unlock, volatile關鍵字可以產生記憶體屏障,防止指令重排序時越過

JMM是允許編譯器和處理器對指令重排序的,但是規定了as-if-serial語義,即不管怎麼重排序,程式的執行結果不能改變。比如下面的程式段:

double pi = 3.14;    //A
double r = 1;        //B
double s= pi * r * r;//C

上面的語句,可以按照A->B->C

執行,結果為3.14,但是也可以按照B->A->C的順序執行,因為A、B是兩句獨立的語句,而C則依賴於A、B,所以A、B可以重排序,但是C卻不能排到A、B的前面。JMM保證了重排序不會影響到單執行緒的執行,但是在多執行緒中卻容易出問題。

比如這樣的程式碼:

int a = 0;
bool flag = false;

public void write() {
    a = 2;              //1
    flag = true;        //2
}

public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
    }
    
}

假如有兩個執行緒執行上述程式碼段,執行緒1先執行write,隨後執行緒2再執行multiply,最後ret的值一定是4嗎?結果不一定:


如圖所示,write方法裡的1和2做了重排序,執行緒1先對flag賦值為true,隨後執行到執行緒2,ret直接計算出結果,再到執行緒1,這時候a才賦值為2,很明顯遲了一步。

這時候可以為flag加上volatile關鍵字,禁止重排序,可以確保程式的“有序性”,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一塊區域裡的程式碼都是一次性執行完畢的。

另外,JMM具備一些先天的有序性,即不需要通過任何手段就可以保證的有序性,通常稱為happens-before原則。<<JSR-133:Java Memory Model and Thread Specification>>定義瞭如下happens-before規則:

  1. 程式順序規則: 一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作
  2. 監視器鎖規則:對一個執行緒的解鎖,happens-before於隨後對這個執行緒的加鎖
  3. volatile變數規則: 對一個volatile域的寫,happens-before於後續對這個volatile域的讀
  4. 傳遞性:如果A happens-before B ,且 B happens-before C, 那麼 A happens-before C
  5. start()規則: 如果執行緒A執行操作ThreadB_start()(啟動執行緒B) , 那麼A執行緒的ThreadB_start()happens-before 於B中的任意操作
  6. join()原則: 如果A執行ThreadB.join()並且成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()操作成功返回。
  7. interrupt()原則: 對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒程式碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷髮生
  8. finalize()原則:一個物件的初始化完成先行發生於它的finalize()方法的開始

第1條規則程式順序規則是說在一個執行緒裡,所有的操作都是按順序的,但是在JMM裡其實只要執行結果一樣,是允許重排序的,這邊的happens-before強調的重點也是單執行緒執行結果的正確性,但是無法保證多執行緒也是如此。

第2條規則監視器規則其實也好理解,就是在加鎖之前,確定這個鎖之前已經被釋放了,才能繼續加鎖。

第3條規則,就適用到所討論的volatile,如果一個執行緒先去寫一個變數,另外一個執行緒再去讀,那麼寫入操作一定在讀操作之前。

第4條規則,就是happens-before的傳遞性。

後面幾條就不再一一贅述了。