簡述
關鍵字volatile可以說是Java虛擬機器提供的最輕量級的同步機制,當一個變數定義為volatile,它具有記憶體可見性以及禁止指令重排序兩大特性,為了更好地瞭解volatile關鍵字,我們可以先看Java記憶體模型
Java記憶體模型
Java記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒擁有自己的工作記憶體,工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀寫)都必須在工作記憶體中進行,不同的執行緒之間無法直接訪問對方工作記憶體的變數。執行緒、主記憶體、工作記憶體關係:
以經典的i++為例,執行緒A從主記憶體獲取變數i值放入到工作記憶體的變數副本,然後在工作記憶體中將i+1,最後將新值同步到主記憶體中。從中我們可以看出簡單的i++,分了3個步驟,可以明顯發現線上程A從主記憶體獲取i值步驟後,可能有其他執行緒同步主記憶體中變數i的值,當執行緒A想要將i+1結果同步到主記憶體時就會出現不正確的結果,這是典型的執行緒不安全。
volatile特性
當一個執行緒修改了共享變數,其他執行緒能夠立即得知這個修改。Java記憶體模型通過在變數修改後將新值同步回主記憶體,volatile變數能保證新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理(synchronized和final兩個關鍵字也具備)。還是拿i++為例,volatile修飾的i可以確保,從主存中所獲取的變數i一定是最新的。
禁止指令重排序,程式執行的順序按照程式碼的先後順序執行。
在執行程式時為了提高效能,編譯器和處理器常常會對指令做重排序。
①.編譯器重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序
②.處理器重排序:如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序
從java原始碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
1屬於編譯器重排序,2和3屬於處理器重排序。
volatile使用場景
在某些特定場景中,volatile相當於一個輕量級的sychronize,因為不會引起執行緒的上下文切換,但是使用volatile必須滿足兩個條件:
①.運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值
②.變數不需要與其他的狀態變數共同參與不變約束
兩個使用場景:
public class VolatileTest { private volatile boolean shutdownRequested;
複製程式碼public void shutdown() { shutdownRequested = true; } public void doWork(){ while (!shutdownRequested) { // 業務邏輯 } } } 複製程式碼
複製程式碼
複製程式碼public class Singleton { private volatile static Singleton singleton; public static Singleton getInstance() { if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } } 複製程式碼
複製程式碼
volatile實現
從硬體架構上來講,處理器使用寫緩衝區來臨時儲存向記憶體寫入的資料,可以減少對記憶體匯流排的佔用。雖然寫緩衝區有這麼多好處,但此操作僅對它所在的處理器可見,這個特性會對記憶體操作的執行順序產生重要的影響,由於操作緩衝區是非同步操作所以在外面看來,先寫後讀,還是先讀後寫,沒有嚴格的固定順序。
volatile修飾的變數相對於普通變數會多出一個lock字首指令,這個操作相當於一個記憶體屏障(只有一個CPU訪問記憶體時,不需要記憶體屏障;但如果有兩個或更多CPU訪問同一塊記憶體,且其中有一個在觀測另一個,就需要記憶體屏障來保證一致性)。
是否能重排序 | 第二個操作 | |||
第一個操作 | 普通讀 | 普通寫 | volatile讀 | volatile寫 |
普通讀 | LoadStore | |||
普通寫 | StoreStore | |||
volatile讀 | LoadLoad | LoadStore | LoadLoad | LoadStore |
volatile寫 | StoreLoad | StoreStore |
StoreStore屏障:保證在volatile寫之前,其前面的所有普通寫操作都已經重新整理到主記憶體
StoreLoad屏障:避免volatile寫與後面可能有的volatile讀/寫操作重排序
LoadLoad屏障:禁止處理器吧上面的volatile讀與下面的普通讀中排序
LoadStore屏障:禁止處理器把上面的volatile讀與下面的普通寫重排序
示例:
大致過程:public class VolatileTest { int a = 0; volatile int var1 = 1; volatile int var2 = 2;
複製程式碼void readAndWrite() { int i = var1; //volatile讀 int j = var2; //volatile讀 a = i + i; //普通讀 var1 = i + 1; //volatile寫 var2 = j * 2; //volatile寫 } 複製程式碼
} 複製程式碼
感謝
1.《深入理解Java虛擬機器》
2.佔小狼——面試必問的volatile,你瞭解多少?
3.《Java併發程式設計的藝術》