jvm volatile 實現併發 解決副本快取變數在工作記憶體中獲取最後的值
在Java執行緒併發處理中,有一個關鍵字volatile的使用目前存在很大的混淆,以為使用這個關鍵字,在進行多執行緒併發處理的時候就可以萬事大吉。
Java語言是支援多執行緒的,為了解決執行緒併發的問題,在語言內部引入了 同步塊(synchronized) 和 volatile 關鍵字機制。
synchronized(不做過多解釋)
同步塊大家都比較熟悉,通過 synchronized 關鍵字來實現,所有加上synchronized 和 塊語句,在多執行緒訪問的時候,同一時刻只能有一個執行緒能夠用
synchronized 修飾的方法 或者 程式碼塊。
volatile
用volatile修飾的變數,執行緒在每次使用變數的時候,都會讀取變數修改後的最的值。volatile很容易被誤用,用來進行原子性操作。
如果要深入瞭解volatile關鍵字的作用,就必須先來了解一下JVM在執行時候的記憶體分配過程。
在 java 垃圾回收整理一文中,描述了jvm執行時刻記憶體的分配。其中有一個記憶體區域是jvm虛擬機器棧,每一個執行緒執行時都有一個執行緒棧,
執行緒棧儲存了執行緒執行時候變數值資訊。當執行緒訪問某一個物件時候值的時候,首先通過物件的引用找到對應在堆記憶體的變數的值,然後把堆記憶體
變數的具體值load到執行緒本地記憶體中,建立一個變數副本,之後執行緒就不再和物件在堆記憶體變數值有任何關係,而是直接修改副本變數的值,
在修改完之後的某一個時刻(執行緒退出之前),自動把執行緒變數副本的值回寫到物件在堆中變數。這樣在堆中的物件的值就產生變化了。下面一幅圖
描述這寫互動!
那麼在瞭解完JVM在執行時候的記憶體分配過程以後,我們開始真正深入的討論volatile的具體作用
請看程式碼:
1 public class VolatileTest extends Thread { 2 3 boolean flag = false; 4 int i = 0; 5 6 public void run() { 7 while (!flag) { 8 i++; 9 } 10 } 11 12 public static void main(String[] args) throwsException { 13 VolatileTest vt = new VolatileTest(); 14 vt.start(); 15 Thread.sleep(2000); 16 vt.flag = true; 17 System.out.println("stope" + vt.i); 18 } 19 }
上面的程式碼是通過標記flag來控制VolatileTest執行緒while迴圈退出的例子!
下面讓我用虛擬碼來描述一下我們的程式
- 首先建立 VolatileTest vt = new VolatileTest();
- 然後啟動執行緒 vt.start();
- 暫停主執行緒2秒(Main) Thread.sleep(2000);
- 這時的vt執行緒已經開始執行,進行i++;
- 主執行緒暫停2秒結束以後將 vt.flag = true;
- 列印語句 System.out.println("stope" + vt.i); 在此同時由於vt.flag被設定為true,所以vt執行緒在進行下一次while判斷 while (!flag) 返回假 結束迴圈 vt執行緒方法結束退出!
- 主執行緒結束
上面的敘述看似並沒有什麼問題,“似乎”完全正確。那就讓我們把程式執行起來看看效果吧,執行mian方法。2秒鐘以後控制檯列印stope-202753974。
可是奇怪的事情發生了 程式並沒有退出。vt執行緒仍然在執行,也就是說我們在主執行緒設定的 vt.flag = true;沒有起作用。
在這裡我需要說明一下,有的同學可能在測試上面程式碼的時候程式可以正常退出。那是因為你的JVM沒有優化造成的!在DOC下面輸入 java -version 檢視 如果顯示Java HotSpot(TM) ... Server 則JVM會進行優化。
如果顯示Java HotSpot(TM) ... Client 為客戶端模式,需要設定成Server模式 設定方法問Google
問題出現了,為什麼我在主執行緒(main)中設定了vt.flag = true; 而vt執行緒在進行判斷flag的時候拿到的仍然是false?
那麼按照我們上面所講的 “JVM在執行時候的記憶體分配過程” 就很好解釋上面的問題了。
首先 vt執行緒在執行的時候會把 變數 flag 與 i (程式碼3,4行)從“主記憶體” 拷貝到 執行緒棧記憶體(上圖的執行緒工作記憶體)
然後 vt執行緒開始執行while迴圈
7 while (!flag) { 8 i++; 9 }
while (!flag)進行判斷的flag 是線上程工作記憶體當中獲取,而不是從 “主記憶體”中獲取。
i++; 將執行緒記憶體中的i++; 加完以後將結果寫回至 "主記憶體",如此重複。
然後再說說主執行緒的執行過程。 我只說明關鍵的地方
vt.flag = true;
主執行緒將vt.flag的值同樣 從主記憶體中拷貝到自己的執行緒工作記憶體 然後修改flag=true. 然後再將新值回到主記憶體。
這就解釋了為什麼在主執行緒(main)中設定了vt.flag = true; 而vt執行緒在進行判斷flag的時候拿到的仍然是false。那就是因為vt執行緒每次判斷flag標記的時候是從它自己的“工作記憶體中”取值,而並非從主記憶體中取值!
這也是JVM為了提供效能而做的優化。那我們如何能讓vt執行緒每次判斷flag的時候都強制它去主記憶體中取值呢。這就是volatile關鍵字的作用。
再次修改我們的程式碼
public class VolatileTest extends Thread { volatile boolean flag = false; int i = 0; public void run() { while (!flag) { i++; } } public static void main(String[] args) throws Exception { VolatileTest vt = new VolatileTest(); vt.start(); Thread.sleep(2000); vt.flag = true; System.out.println("stope" + vt.i); } }
在flag前面加上volatile關鍵字,強制執行緒每次讀取該值的時候都去“主記憶體”中取值。在試試我們的程式吧,已經正常退出了