1. 程式人生 > >Java程式設計:併發程式設計的3個特性

Java程式設計:併發程式設計的3個特性

在併發程式設計中我們經常會遇到三個問題:原子性問題、可見性問題、有序性問題,下面為大家剖析一下這三個問題。如有不正之處,歡迎批評指正。

一、原子性

原子行:即一個或者多個操作作為一個整體,要麼全部執行,要麼都不執行,並且操作在執行過程中不會被執行緒排程機制打斷;而且這種操作一旦開始,就一直執行到結束,中間不會有任何上下文切換(context switch)。

我們用銀行賬戶轉賬問題來形象的解釋一下原子性(當然銀行賬戶轉賬涉及到的問題比較多,我們這裡只是來比擬一下)

舉例一:
比如張三向李四轉賬200元,可以分解成如下步驟:
1)從張三賬戶減去200元
2)給李四賬戶加上200元
如果只執行步驟1),沒有執行步驟2),問題就來了,張三說他給李四轉錢了,李四說他沒收到,銀行該怎麼處理這個事情呢?將該操作加上原子性就可以很好的解決轉賬問題。

舉例二:
在java開發中我們經常使用如下語句

int i = 0;   //語句1
i++;         //語句2

語句1是一個原子性操作。

語句2的分解步驟是:
1)獲取 i 的值;
2)計算 i + 1 的值;
3)將 i + 1 的值賦給 i;
執行以上3個步驟的時候是可以進行執行緒切換的,因此語句2不是一個原子性操作

二、可見性

可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值。

舉例:

private int i = 0;
private int j = 0;
//執行緒1
i = 10;

//執行緒2
j = i;

執行緒1修改i的值為10時的執行步驟:
1)將10賦給執行緒1工作記憶體中的 i 變數;
2)將執行緒1工作記憶體中的 i 變數的值賦給主記憶體中的 i 變數;

當執行緒2執行j = i時,執行緒2的執行步驟:
1)將主記憶體中的 i 變數的值讀取到執行緒2的工作記憶體中;
2)將主記憶體中的 j 變數的值讀取到執行緒2的工作記憶體中;
3)將執行緒2工作記憶體中的 i 變數的值賦給執行緒2工作記憶體中的 j 變數;
4)將執行緒2工作記憶體中的 j 變數的值賦給主記憶體中的 j 變數;

如果執行緒1執行完步驟1,執行緒2開始執行,此時主記憶體中 i 變數的值仍然為 0,那麼執行緒2獲取到的 i 變數的值為 0,而不是 10。

這就是可見性問題,執行緒1對 i 變數做了修改之後,執行緒2沒有立即看到執行緒1修改的值。

三、有序性

有序性:即程式執行的順序按照程式碼的先後順序執行。

舉例一:

int i = 0;
int j = 0;
i = 10;   //語句1
j = 1;    //語句2

語句可能的執行順序如下:
1)語句1 語句2
2)語句2 語句1

語句1一定在語句2前面執行嗎?答案是否定的,這裡可能會發生執行重排(Instruction Reorder)。一般來說,處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式在單執行緒環境下最終執行結果和程式碼順序執行的結果是一致的。
比如上面的程式碼中,語句1和語句2誰先執行對最終的程式結果並沒有影響,那麼就有可能在執行過程中,語句2先執行而語句1後執行。

舉例二:

int i = 0;    //語句1
int j = 0;    //語句2
i = i + 10;   //語句3
j = i * i;    //語句4

語句可能的執行順序如下:
1)語句1 語句2 語句3 語句4
2)語句2 語句1 語句3 語句4
3)語句1 語句3 語句2 語句4

語句3是不可能在語句4之後執行的,因為編譯器在進行指令重排時會考慮資料的依賴性問題,語句4依賴於語句3,因此語句3一定在語句4之前執行。

接下來我們說一下多執行緒環境。

舉例三:

private boolean flag = false;    
private Context context = null;  
//執行緒1
context = loadContext();  //語句1
flag = true;              //語句2

//執行緒2 
while(!flag){
    Thread.sleep(1000L);
}
dowork(context);

語句可能的執行順序如下:
1)語句1 語句2
2)語句2 語句1

由於線上程1中語句1、語句2是沒有依賴性的,所以可能會出現指令重排。如果發生了指令重排,執行緒1先執行語句2,這時候執行緒2開始執行,此時flag值為true,因此執行緒2繼續執行dowrk(context),此時context並沒有初始化,因此就會導致程式錯誤。

因此可以得出結論,指令重排不會影響單執行緒的執行結果,但是會影響多執行緒併發執行的結果正確性。

總結:一個正確執行的併發程式,必須具備原子性、可見性、有序性。否則就有可能導致程式執行結果不正確,甚至引起死迴圈。