本文首發於微信公衆號:老胡碼字java
前面一篇文章在介紹Java內存模型的三大特性(原子性、可見性、有序性)時,在可見性和有序性中都提到了volatile關鍵字,那這篇文章就來介紹volatile關鍵字的內存語義以及實現其特性的內存屏障。緩存
volatile是JVM提供的一種最輕量級的同步機制,由於Java內存模型爲volatile定義特殊的訪問規則,使其能夠實現Java內存模型中的兩大特性:可見性和有序性。正由於volatile關鍵字具備這兩大特性,因此咱們可使用volatile關鍵字解決多線程中的某些同步問題。安全
volatile的可見性是指當一個變量被volatile修飾後,這個變量就對全部線程都可見。白話點就是說當一個線程修改了一個volatile修飾的變量後,其餘線程能夠馬上得知這個變量的修改,拿到最這個變量最新的值。bash
結合前一篇文章提到的Java內存模型中線程、工做內存、主內存的交互關係,咱們對volatile的可見性也能夠這麼理解,定義爲volatile修飾的變量,在線程對其進行寫入操做時不會把值緩存在工做內存中,而是直接把修改後的值刷新回寫到主內存,而當處理器監控到其餘線程中該變量在主內存中的內存地址發生變化時,會讓這些線程從新到主內存中拷貝這個變量的最新值到工做內存中,而不是繼續使用工做內存中舊的緩存。微信
下面我列舉一個利用volatile可見性解決多線程併發安全的示例:多線程
public class VolatileDemo {
//private static boolean isReady = false;
private static volatile boolean isReady = false;
static class ReadyThread extends Thread {
public void run() {
while (!isReady) {
}
System.out.println("ReadyThread finish");
}
}
public static void main(String[] args) throws InterruptedException {
new ReadyThread().start();
Thread.sleep(1000);//sleep 1秒鐘確保ReadyThread線程已經開始執行
isReady = true;
}
}
複製代碼
上面這段代碼運行以後最終會在控制檯打印出:ReadyThread finish,而當你將變量isReady的volatile修飾符去掉以後再運行則會發現程序一直運行而不結束,而控制檯也沒有任何打印輸出。併發
咱們分析下這個程序:初始時isReady爲false,因此ReadyThread線程啓動開始執行後,它的while代碼塊因標誌位isReady爲false會進入死循環,當用volatile關鍵字修飾isReady時,main方法所在的線程將isReady修改成true以後,ReadyThread線程會馬上得知並獲取這個最新的isReady值,緊接着while循環就會結束循環,因此最後打印出了相關文字。而當未用volatile修飾時,main方法所在的線程雖然修改了isReady變量,但ReadyThread線程並不知道這個修改,因此使用的仍是以前的舊值,所以會一直死循環執行while語句。優化
有序性是指程序代碼的執行是按照代碼的實現順序來按序執行的。spa
volatile的有序性特性則是指禁止JVM指令重排優化。線程
咱們來看一個例子:
public class Singleton {
private static Singleton instance = null;
//private static volatile Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
//第一次判斷
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
//初始化,並不是原子操做
instance = new Singleton();
}
}
}
return instance;
}
}
複製代碼
上面的代碼是一個很常見的單例模式實現方式,可是上述代碼在多線程環境下是有問題的。爲何呢,問題出在instance對象的初始化上,由於instance = new Singleton();
這個初始化操做並非原子的,在JVM上會對應下面的幾條指令:
memory =allocate(); //1. 分配對象的內存空間
ctorInstance(memory); //2. 初始化對象
instance =memory; //3. 設置instance指向剛分配的內存地址
複製代碼
上面三個指令中,步驟2依賴步驟1,可是步驟3不依賴步驟2,因此JVM可能針對他們進行指令重拍序優化,重排後的指令以下:
memory =allocate(); //1. 分配對象的內存空間
instance =memory; //3. 設置instance指向剛分配的內存地址
ctorInstance(memory); //2. 初始化對象
複製代碼
這樣優化以後,內存的初始化被放到了instance分配內存地址的後面,這樣的話當線程1執行步驟3這段賦值指令後,恰好有另一個線程2進入getInstance方法判斷instance不爲null,這個時候線程2拿到的instance對應的內存其實還未初始化,這個時候拿去使用就會致使出錯。
因此咱們在用這種方式實現單例模式時,會使用volatile關鍵字修飾instance變量,這是由於volatile關鍵字除了能夠保證變量可見性以外,還具備防止指令重排序的做用。當用volatile修飾instance以後,JVM執行時就不會對上面提到的初始化指令進行重排序優化,這樣也就不會出現多線程安全問題了。
volatile的能夠在如下場景中使用:
volatile關鍵字能保證變量的可見性和代碼的有序性,可是不能保證變量的原子性,下面我再舉一個volatile與原子性的例子:
public class VolatileTest {
public static volatile int count = 0;
public static void increase() {
count++;
}
public static void main(String[] args) {
Thread[] threads = new Thread[20];
for(int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for(int j = 0; j < 1000; j++) {
increase();
}
});
threads[i].start();
}
//等待全部累加線程結束
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}
}
複製代碼
上面這段代碼建立了20個線程,每一個線程對變量count進行1000次自增操做,若是這段代碼併發正常的話,結果應該是20000,但實際運行過程當中常常會出現小於20000的結果,由於count++這個自增操做不是原子操做。
上面的count++自增操做等價於count=count+1,因此JVM須要先讀取count的值,而後在count的基礎上給它加1,而後再將新的值從新賦值給count變量,因此這個自增總共須要三步。
上圖中我將線程對count的自增操做畫了個簡單的流程,一個線程要對count進行自增時要先讀取count的值,而後在當前count值的基礎上進行count+1操做,最後將count的新值從新寫回到count。
若是線程2在線程1讀取count舊值寫回count新值期間讀取count的值,顯然這個時候線程2讀取的是count還未更新的舊值,這時兩個線程是對同一個值進行了+1操做,這樣這兩個線程就沒有對count實現累加效果,相反這些操做卻又沒有違反volatile的定義,因此這種狀況下使用volatile依然會存在多線程併發安全的問題。
前面介紹了volatile的可見性和有序性,那JVM究竟是如何爲volatile關鍵字實現的這兩大特性呢,Java內存模型實際上是經過內存屏障(Memory Barrier)來實現的。
內存屏障其實也是一種JVM指令,Java內存模型的重排規則會要求Java編譯器在生成JVM指令時插入特定的內存屏障指令,經過這些內存屏障指令來禁止特定的指令重排序。
另外內存屏障還具備必定的語義:內存屏障以前的全部寫操做都要回寫到主內存,內存屏障以後的全部讀操做都能得到內存屏障以前的全部寫操做的最新結果(實現了可見性)。所以重排序時,不容許把內存屏障以後的指令重排序到內存屏障以前。
下面的表是volatile有關的禁止指令重排的行爲:
第一個操做 | 第二個操做:普通讀寫 | 第二個操做:volatile讀 | 第二個操做:volatile寫 |
---|---|---|---|
普通讀寫 | 能夠重排 | 能夠重排 | 不能夠重排 |
volatile讀 | 不能夠重排 | 不能夠重排 | 不能夠重排 |
volatile寫 | 能夠重排 | 不能夠重排 | 不能夠重排 |
從上面的表咱們能夠得出下面這些結論:
JVM中提供了四類內存屏障指令:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保證load1的讀取操做在load2及後續讀取操做以前執行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其後的寫操做執行前,保證store1的寫操做已刷新到主內存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其後的寫操做執行前,保證load1的讀操做已讀取結束 |
StoreLoad | Store1; StoreLoad; Load2 | 保證store1的寫操做已刷新到主內存以後,load2及其後的讀操做才能執行 |
volatile實現了Java內存模型中的可見性和有序性,它的這兩大特性則是經過內存屏障來實現的,同時volatile沒法保證原子性。