1. 程式人生 > >Java併發程式設計:volatile關鍵字解析(二.併發程式設計中的三個概念)

Java併發程式設計:volatile關鍵字解析(二.併發程式設計中的三個概念)

在併發程式設計中,我們通常會遇到以下三個問題:原子性問題可見性問題有序性問題。我們先看具體看一下這三個概念:

1.原子性

  原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

  一個很經典的例子就是銀行賬戶轉賬問題:

  比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。

  試想一下,如果這2個操作不具備原子性,會造成什麼樣的後果。假如從賬戶A減去1000元之後,操作突然中止。然後又從B取出了500元,取出500元之後,再執行 往賬戶B加上1000元 的操作。這樣就會導致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個轉過來的1000元。

  所以這2個操作必須要具備原子性才能保證不出現一些意外的問題。

  同樣地反映到併發程式設計中會出現什麼結果呢?

  舉個最簡單的例子,大家想一下假如為一個32位的變數賦值過程不具備原子性的話,會發生什麼後果?

i = 9;
  假若一個執行緒執行到這個語句時,我暫且假設為一個32位的變數賦值包括兩個過程:為低16位賦值,為高16位賦值。

  那麼就可能發生一種情況:當將低16位數值寫入之後,突然被中斷,而此時又有一個執行緒去讀取i的值,那麼讀取到的就是錯誤的資料。

2.可見性

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

  舉個簡單的例子,看下面這段程式碼:

//執行緒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修改的值。

3.有序性

  有序性:即程式執行的順序按照程式碼的先後順序執行。舉個簡單的例子,看下面這段程式碼:

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

上面程式碼定義了一個int型變數,定義了一個boolean型別變數,然後分別對兩個變數進行賦值操作。從程式碼順序上看, 語句1是在語句2前面的,那麼JVM在真正執行這段程式碼的時候會保證語句1一定會在語句2前面執行嗎?不一定,為什麼呢? 這裡可能會發生指令重排序(Instruction Reorder)。

下面解釋一下什麼是指令重排序,一般來說,處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式

中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的

比如上面的程式碼中,語句1和語句2誰先執行對最終的程式結果並沒有影響,那麼就有可能在執行過程中,語句2先執行而

語句1後執行。

  但是要注意,雖然處理器會對指令進行重排序,但是它會保證程式最終結果會和程式碼順序執行結果相同,那麼它靠什麼保證的呢?

再看下面一個例子:

int a = 10;    //語句1
int r = 2;    //語句2
a = a + 3;    //語句3
r = a*a;     //語句4

       這段程式碼有4個語句,那麼可能的一個執行順序是:

  

  那麼可不可能是這個執行順序呢: 語句2   語句1    語句4   語句3

  不可能,因為處理器在進行重排序時是會考慮指令之間的資料依賴性(這裡語句4依賴於語句3),如果一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。

  雖然重排序不會影響單個執行緒內程式執行的結果,但是多執行緒呢?下面看一個例子:

//執行緒1:
context = loadContext();   //語句1
inited = true;             //語句2
 
//執行緒2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面程式碼中,由於語句1和語句2沒有資料依賴性,因此可能會被重排序。假如發生了重排序,線上程1執行過程中先執行語句2, 而此是執行緒2會以為初始化工作已經完成,那麼就會跳出while迴圈,去執行doSomethingwithconfig(context)方法,而此時 context並沒有被初始化,就會導致程式出錯。

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

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