Java併發編程的3個特性

1、原子性

原子行:即一個或者多個操做做爲一個總體,要麼所有執行,要麼都不執行,而且操做在執行過程當中不會被線程調度機制打斷;並且這種操做一旦開始,就一直運行到結束,中間不會有任何上下文切換(context switch)。java

咱們用銀行帳戶轉帳問題來形象的解釋一下原子性(固然銀行帳戶轉帳涉及到的問題比較多,咱們這裏只是來比擬一下)多線程

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

舉例二:
在java開發中咱們常用以下語句優化

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

 

語句1是一個原子性操做。spa

語句2的分解步驟是:
1)獲取 i 的值;
2)計算 i + 1 的值;
3)將 i + 1 的值賦給 i;
執行以上3個步驟的時候是能夠進行線程切換的,所以語句2不是一個原子性操做線程

2、可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看到修改的值。code

舉例:內存

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修改的值。

3、有序性

有序性:即程序執行的順序按照代碼的前後順序執行。

舉例一:

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

所以能夠得出結論,指令重排不會影響單線程的執行結果,可是會影響多線程併發執行的結果正確性。

總結:一個正確執行的併發程序,必須具有原子性、可見性、有序性。不然就有可能致使程序運行結果不正確,甚至引發死循環。

相關文章
相關標籤/搜索