併發程式設計三大特性
併發程式設計中往往涉及三個問題:原子性、可見性、有序性。
原子性
定義:即一個或者多個操作作為一個整體,要麼全部執行,要麼都不執行,並且操作在執行過程中不會被執行緒排程機制打斷;而且這種操作一旦開始,就一直執行到結束,中間不會有任何上下文切換。
例如轉賬問題,A向B轉1000元,該過程分解成兩個步驟:
1、A賬戶減掉1000元;
2、B賬戶增加1000元。
上述兩個步驟如果中途被打斷會造成什麼後果?A賬戶已經被扣掉1000了,但是B賬戶確沒有收到1000,這種情況肯定要禁止的。所以這2個操作必須要具備原子性才能保證不出現一些意外的問題。
在程式碼程式設計中經常使用如下語句:
- i = 0; //1
- j = i ; //2
- i++; //3
- 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並沒有初始化,因此就會導致程式錯誤。
從上面可以看出,指令重排序不會影響單個執行緒的執行,但是會影響到執行緒併發執行的正確性。
總結:要想併發程式正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程式執行不正確。