標籤:多線程 java 並發
上一篇部落格JAVA並發編程3_線程同步之synchronized關鍵字中講解了JAVA中保證線程同步的關鍵字synchronized,其實JAVA裡面還有個較弱的同步機制volatile。volatile關鍵字是JAVA中的輕量級的同步機制,用來將變數的更新操作同步到其他線程。從記憶體可見度的角度來說,寫入volatile變數相當於退出同步代碼塊,讀取volatile變數相當於進入同步代碼塊。
舊的記憶體模型:保證讀寫volatile都直接發生在main memory中。
在新的記憶體模型下(1.5)對volatile的語義進行了修補和增強:如果當線程 A 寫入 volatile 變數 V 而線程 B 讀取 V 時,那麼在寫入 V 時,A 可見的所有變數值現在都可以保證對 B 是可見的。
一句話:volatile保證可見度,但不能保證原子性。
原子性的:一組語句作為一個不可分割的單元被執行。任何一個執行同步代碼塊的線程,都不可能看到有其他線程正在執行由同一個鎖保護的同步代碼塊。volatile變數的非原子性最容易被忽略。
可見度:指一個線程修改了一個共用變數的值,其他線程能夠立即得知這個修改。
volatile的非原子性
變數被定義為volatile並不能保證對其所有操作是原子的,由於非原子性,因此volatile並不能保證多線程並發的安全性。如下面的代碼:
public class Test implements Runnable{public volatile int race = 0;@Overridepublic void run() {increase();}private void increase() {race ++;}public static void main(String[] args) {Test t = new Test();Thread [] threads = new Thread[1000];for (int i = 0; i < 1000; i++) {threads[i] = new Thread(t);threads[i].start();}while (Thread.activeCount() > 1) {Thread.yield();}// 保證列印的時候1000個線程都已經執行完畢System.out.println(t.race);}}
這段代碼開啟了1000個線程,對race變數進行自增操作。理論上,安全執行緒的話,執行結果應該是1000。但實際上執行得到的結果都是一個小於1000的值。
分析一下上面案的代碼,問題就出在了race++這句代碼。它不是原子操作。這句代碼實際上是分為三個操作的:讀取race的值、進行加1操作、寫入新的值。
顯然可以看出來,將變數定義成vilatile也不能保證原子性:
線程1先讀取了變數race的原始值,然後線程1被阻塞了;線程2也去讀取變數race的原始值,然後進行加1操作,並把+1後的值寫入工作記憶體,最後寫入主存,然後線程1接著進行加1操作,由於已經讀取了race的值,此時線上程1的工作記憶體中race的值仍然是之前的值,所以線程1對race進行加1操作後的值和剛才一樣,然後將這個值寫入工作記憶體,最後寫入主存。這樣就出現了兩個線程自增完後其實只加了一次。究其原因是因為volatile不能保證原子性。
可以將自增操作改為同步代碼塊即可解決。
private synchronized void increase() {race ++;}
volatile的可見度
一個線程修改了某個volatile變數的值,這新值對其他線程來說是立即可見的。
boolean ready;// thread 1while (!ready) { doSomthing();}// thread2ready = true;
這是銷毀線程的通用方法。但是存在問題是ready變數改為true還沒來得及寫入主存,就轉到其他線程執行了,這時還會進入迴圈。這時,volatile的作用就體現出來了。volatile變數保證了他在一個線程裡面修改後會立即被其他線程得知。
volatile變數的使用情境這篇文章寫得很詳細:Java 理論與實踐: 正確使用 Volatile 變數
JAVA並發編程4_線程同步之volatile關鍵字