volatile定義
Java程式設計語言允許線程訪問共用變數,為了確保共用變數能被準確和一致地更新,線程應該確保通過獨佔鎖定單獨獲得這個變數。Java語言提供了volatile,在某些情況下比鎖要更加方便。如果一個欄位被聲明成volatile,Java線程記憶體模型確保所有線程看到這個變數的值是一致的。 volatile的作用
先讓我們說說volatile關鍵字的作用。它在多處理器開發中保證了共用變數的“可見度”。可見度的意思是當一個線程修改一個共用變數時,另外一個線程能讀到這個修改的值。如果volatile變數修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起線程內容相關的切換和調度。 volatile程式碼範例 單例模式(重排序)
public class Singleton { public static volatile Singleton singleton; /** * 建構函式私人 */ private Singleton(){} /** * 單例實現 * @author fuyuwei * 2017年5月14日 上午10:07:07 * @return */ public static Singleton getInstance(){ if(singleton == null){ synchronized (singleton) { if(singleton == null){ singleton = new Singleton(); } } } return singleton; }}
我們知道執行個體化一個對象經過分配記憶體空間、初始化對象、將記憶體空間的地址賦值給對應的引用,上面的單例模式可以解釋為分配記憶體空間、將記憶體位址賦值給對應的應用、初始化對象。上面的代碼如果我們不加volatile在並發環境下可能會出現Singleton的多次執行個體化,假如線程A進入getInstance方法,發現singleton==null,然後加鎖通過new Singleton進行執行個體化,然後釋放鎖,我們知道new Singleton在JVM中其實是分為3步,假如線程啊在釋放鎖之後還沒來得及通知其他線程,這時候線程B進入getInstance的時候發現singleton==null便會再次執行個體化。 可見度
一個線程修改了共用變數值,而另一個線程卻看不到。引起可見度問題的主要原因是每個線程擁有自己的一個快取區——線程工作記憶體。volatile關鍵字能有效解決這個問題
public class Volatile { int m = 0; int n = 1; public void set(){ m = 6; n = m; } public void print(){ System.out.println("m:"+m+",n:"+n); } public static void main(String[] args) { while(true){ final Volatile v = new Volatile(); new Thread(new Runnable(){ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } v.set(); } }).start(); new Thread(new Runnable(){ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } v.print(); } }).start(); } }}
正常情況下m=0,n=1;m=6,n=6,通過運行我們發現了m=0,n6(需要長時間運行)
m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:0,n:1m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:6,n:6m:0,n:6m:6,n:6m:6,n:6m:6,n:6m:0,n:1m:0,n:1m:0,n:1m:6,n:6m:0,n:1m:0,n:1m:0,n:1m:6,n:6m:6,n:6m:6,n:6m:0,n:1m:6,n:6m:6,n:6
對volatile變數的寫操作與普通變數的主要區別有兩點:
(1)修改volatile變數時會強制將修改後的值重新整理的主記憶體中。
(2)修改volatile變數後會導致其他線程工作記憶體中對應的變數值失效。因此,再讀取該變數值的時候就需要重新從讀取主記憶體中的值。 volatile並不能保證原理性
package com.swk.thread;public class Volatile { private volatile int m = 0; public void incr(){ m++; } public static void main(String[] args) { final Volatile v = new Volatile(); for(int i=0;i<1000;i++){ new Thread(new Runnable(){ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } v.incr();; } }).start(); } try { Thread.sleep(10000);// 確保1000次迴圈執行完畢 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(v.m); }}
輸出結果:950,並不是我們想象中的1000,如果我們在incr加上synchronized,輸出結果是1000
原因也很簡單,i++其實是一個複合操作,包括三步驟:
(1)讀取i的值。
(2)對i加1。
(3)將i的值寫回記憶體。
volatile是無法保證這三個操作是具有原子性的,我們可以通過AtomicInteger或者Synchronized來保證+1操作的原子性。 volatile底層實現
在瞭解volatile實現原理之前,我們先來看下與其實現原理相關的CPU術語與說明
| 術語 |
英文單詞 |
術語描述 |
| 記憶體屏障 |
memory barries |
是一組處理器指令,用於實現對記憶體操作的順序限制 |
| 緩衝行 |
cache line |
緩衝中可以分配的最小儲存單位。處理器填寫緩衝線時會載入整個緩衝線,需要使用多個主記憶體周期 |
| 原子操作 |
atomic operations |
不可中斷的一個或一些列操作 |
| 緩衝行填充 |
cache line fill |
當處理其識別到記憶體中讀取運算元是可快取的,處理器讀取整個緩衝行到適當的緩衝 |
| 快取命中 |
cache hit |
如果進行快取行填充操作的記憶體位置仍然是下次處理器訪問的地址時,處理器從快取中讀取,而不是從記憶體中讀取 |
| 寫命中 |
write hit |
當處理器運算元寫回到一個記憶體快取區域時,他首先會檢查這個緩衝的記憶體位址是否在緩衝行中,如果存在一個有效緩衝行,則處理器將這個運算元寫回到緩衝,而不是會寫到記憶體 |
| 寫缺失 |
write misses the cache |
一個有效緩衝行被寫入到不存在的記憶體地區 |
volatile是如何來保證可見度的呢。讓我們在X86處理器下通過工具擷取JIT編譯器產生的彙編指令來查看對volatile進行寫操作時,CPU會做什麼事情。Java代碼如下:
private valatile Singleton instance = new Singleton();
轉成彙編代碼如下
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
當有volatile變數修飾時會出現lock addl $0×0,(%esp),Lock首碼的指令在多核處理器下會引發了兩件事情
1)將當前處理器緩衝行的資料寫回到系統記憶體。
2)這個寫回記憶體的操作會使在其他CPU裡緩衝了該記憶體位址的資料無效。
為了提高處理速度,處理器不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部緩衝(L1,L2或其他)後再進行操作,但操作完不知道何時會寫到記憶體。如果對聲明了volatile的變數進行寫操作,JVM就會向處理器發送一條Lock首碼的指令,將這個變數所在緩衝行的資料寫回到系統記憶體。但是,就算寫回到記憶體,如果其他處理器緩衝的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩衝是一致的,就會實現緩衝一致性協議,每個處理器通過嗅探在匯流排上傳播的資料來檢查自己緩衝的值是不是到期了,當處理器發現自己緩衝行對應的記憶體位址被修改,就會將當前處理器的緩衝行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器緩衝裡。 volatile使用情境 一個線程寫,多個線程讀
volatile boolean shutdownRequested;...public void shutdown() { shutdownRequested = true; }public void doWork() { while (!shutdownRequested) { // do stuff }}
結合使用 volatile 和 synchronized 實現 “開銷較低的讀-寫鎖”
volatile 允許多個線程執行讀操作,因此當使用 volatile 保證讀代碼路徑時,要比使用鎖執行全部代碼路徑獲得更高的共用度 —— 就像讀-寫操作一樣。
public class CheesyCounter { private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; }}