1. 程式人生 > >併發程式設計三大特性

併發程式設計三大特性

併發程式設計中往往涉及三個問題:原子性、可見性、有序性。

原子性

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

例如轉賬問題,A向B轉1000元,該過程分解成兩個步驟:

1、A賬戶減掉1000元;

2、B賬戶增加1000元。

上述兩個步驟如果中途被打斷會造成什麼後果?A賬戶已經被扣掉1000了,但是B賬戶確沒有收到1000,這種情況肯定要禁止的。所以這2個操作必須要具備原子性才能保證不出現一些意外的問題。

在程式碼程式設計中經常使用如下語句:

  1. i = 0;       //1
  2. j = i ;      //2
  3. i++;         //3
  4. i = j + 1;   //4

分析上述四種語句,哪些是原子操作,哪些不是。

1在Java中,對基本資料型別的變數和賦值操作都是原子性操作;

2中包含了兩個操作:讀取i,將i值賦值給j

3中包含了三個操作:讀取i值、i + 1 、將+1結果賦值給i;

4中同三一樣

從上面分析中,其實可以初步總結出,語句中包含多個操作的情況,通常都不具備原子性,那麼在實際開發過程中如果只是單執行緒的話是沒問題的,但是多執行緒就很有可能得到意想不到的結果了。主要原因就是在操作過程中其它執行緒中途可能會寫入資料,導致讀取到的可能是髒資料。

要想在多執行緒環境下保證原子性,則可以通過鎖、synchronized來確保。volatile是無法保證複合操作的原子性。

可見性

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

例子:

//執行緒1執行的程式碼
int i = 0;
i = 10;

//執行緒2執行的程式碼
j = i;

如果執行執行緒1的是CPU1,執行執行緒2的是CPU2。由上面的分析可知,當執行緒1執行 i = 10這句時,會先把i的初始值載入到CPU1的快取記憶體中,然後賦值為10,那麼在CPU1的快取記憶體當中i的值變為10了,卻沒有立即寫入到主存當中。此時執行緒2執行 j = i,它會先去主存讀取i的值並載入到CPU2的快取當中,注意此時記憶體當中i的值還是0,那麼就會使得j的值為0,而不是10。這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值。

在上面已經分析了,在多執行緒環境下,一個執行緒對共享變數的操作對其它執行緒是不可見的。

有序性

定義:即程式執行的順序按照程式碼的先後順序執行。

例子:

private int i = 0;              
private bool flag = false;
i = 1;                //語句1  
flag = true;          //語句2

從程式碼順序上看,語句1是在語句2前面的,但是JVM在真正執行這段程式碼的時候不會保證語句1一定會在語句2前面執行,因為這裡可能會發生指令重排序(Instruction Reorder)。指令重排序,一般來說,處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的。

雖然處理器會對指令進行重排序,但是它會保證程式最終結果會和程式碼順序執行結果相同,那麼它靠什麼保證的呢?再看下面一個例子:
int i = 0;    //語句1
int j = 0;    //語句2
i = i + 10;   //語句3
j = i * i;    //語句4

這段程式碼有4個語句,那麼可能的一個執行順序是:
語句2 -> 語句1 -> 語句3 -> 語句4
那麼可不可能是這個執行順序:
語句2 -> 語句1 -> 語句4 -> 語句3。
不可能,因為處理器在進行重排序時是會考慮指令之間的資料依賴性,如果一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。雖然重排序不會影響單個執行緒內程式執行的結果,但是多執行緒呢?下面看一個例子:

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

從上面可以看出,指令重排序不會影響單個執行緒的執行,但是會影響到執行緒併發執行的正確性。

總結:要想併發程式正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程式執行不正確。