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++
那樣的操作,必須藉助於synchronized
和Lock
來保證整塊程式碼的原子性了。執行緒在釋放鎖之前,必然會把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
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規則:
- 程式順序規則: 一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作
- 監視器鎖規則:對一個執行緒的解鎖,happens-before於隨後對這個執行緒的加鎖
- volatile變數規則: 對一個volatile域的寫,happens-before於後續對這個volatile域的讀
- 傳遞性:如果A happens-before B ,且 B happens-before C, 那麼 A happens-before C
- start()規則: 如果執行緒A執行操作
ThreadB_start()
(啟動執行緒B) , 那麼A執行緒的ThreadB_start()
happens-before 於B中的任意操作 - join()原則: 如果A執行
ThreadB.join()
並且成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()
操作成功返回。 - interrupt()原則: 對執行緒
interrupt()
方法的呼叫先行發生於被中斷執行緒程式碼檢測到中斷事件的發生,可以通過Thread.interrupted()
方法檢測是否有中斷髮生 - finalize()原則:一個物件的初始化完成先行發生於它的
finalize()
方法的開始
第1條規則程式順序規則是說在一個執行緒裡,所有的操作都是按順序的,但是在JMM裡其實只要執行結果一樣,是允許重排序的,這邊的happens-before強調的重點也是單執行緒執行結果的正確性,但是無法保證多執行緒也是如此。
第2條規則監視器規則其實也好理解,就是在加鎖之前,確定這個鎖之前已經被釋放了,才能繼續加鎖。
第3條規則,就適用到所討論的volatile,如果一個執行緒先去寫一個變數,另外一個執行緒再去讀,那麼寫入操作一定在讀操作之前。
第4條規則,就是happens-before的傳遞性。
後面幾條就不再一一贅述了。