在這個小結里面重點討論原子操作的原理和設計思想。
由于在下一個章節中會談到鎖機制,因此此小節中會適當引入鎖的概念。
在Java Concurrency in Practice中是這樣定義線程安全的:
當多個線程訪問一個類時,如果不用考慮這些線程在運行時環境下的調度和交替運行,并且不需要額外的同步及在調用方代碼不必做其他的協調,這個類的行為仍然是正確的,那么這個類就是線程安全的。
顯然只有資源競爭時才會導致線程不安全,因此無狀態對象永遠是線程安全的。
原子操作的描述是: 多個線程執行一個操作時,其中任何一個線程要么完全執行完此操作,要么沒有執行此操作的任何步驟,那么這個操作就是原子的。
枯燥的定義介紹完了,下面說更枯燥的理論知識。
指令重排序
Java語言規范規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同于它在嚴格的順序化環境下的結果,那么指令的執行順序就可能與代碼的順序不一致。這個過程通過叫做指令的重排序。指令重排序存在的意義在于:JVM能夠根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的重新排序機器指令,使機器指令更符合CPU的執行特點,最大限度的發揮機器的性能。
程序執行最簡單的模型是按照指令出現的順序執行,這樣就與執行指令的CPU無關,最大限度的保證了指令的可移植性。這個模型的專業術語叫做順序化一致性模型。但是現代計算機體系和處理器架構都不保證這一點(因為人為的指定并不能總是保證符合CPU處理的特性)。
我們來看最經典的一個案例。
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 變量
©2009-2014 IMXYLZ
|求賢若渴