溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java多線程之volatile關鍵字及內存屏障實例解析

發(fā)布時間:2020-08-22 07:57:24 來源:腳本之家 閱讀:262 作者:老胡 欄目:編程語言

前面一篇文章在介紹Java內存模型的三大特性(原子性、可見性、有序性)時,在可見性和有序性中都提到了volatile關鍵字,那這篇文章就來介紹volatile關鍵字的內存語義以及實現其特性的內存屏障。

volatile是JVM提供的一種最輕量級的同步機制,因為Java內存模型為volatile定義特殊的訪問規(guī)則,使其可以實現Java內存模型中的兩大特性:可見性和有序性。正因為volatile關鍵字具有這兩大特性,所以我們可以使用volatile關鍵字解決多線程中的某些同步問題。

volatile的可見性

volatile的可見性是指當一個變量被volatile修飾后,這個變量就對所有線程均可見。白話點就是說當一個線程修改了一個volatile修飾的變量后,其他線程可以立刻得知這個變量的修改,拿到最這個變量最新的值。

結合前一篇文章提到的Java內存模型中線程、工作內存、主內存的交互關系,我們對volatile的可見性也可以這么理解,定義為volatile修飾的變量,在線程對其進行寫入操作時不會把值緩存在工作內存中,而是直接把修改后的值刷新回寫到主內存,而當處理器監(jiān)控到其他線程中該變量在主內存中的內存地址發(fā)生變化時,會讓這些線程重新到主內存中拷貝這個變量的最新值到工作內存中,而不是繼續(xù)使用工作內存中舊的緩存。

下面我列舉一個利用volatile可見性解決多線程并發(fā)安全的示例:

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線程已經開始執(zhí)行
  isReady = true;
 }
}

上面這段代碼運行之后最終會在控制臺打印出: ReadyThread finish ,而當你將變量isReady的volatile修飾符去掉之后再運行則會發(fā)現程序一直運行而不結束,而控制臺也沒有任何打印輸出。

我們分析下這個程序:初始時isReady為false,所以ReadyThread線程啟動開始執(zhí)行后,它的while代碼塊因標志位isReady為false會進入死循環(huán),當用volatile關鍵字修飾isReady時,main方法所在的線程將isReady修改為true之后,ReadyThread線程會立刻得知并獲取這個最新的isReady值,緊接著while循環(huán)就會結束循環(huán),所以最后打印出了相關文字。而當未用volatile修飾時,main方法所在的線程雖然修改了isReady變量,但ReadyThread線程并不知道這個修改,所以使用的還是之前的舊值,因此會一直死循環(huán)執(zhí)行while語句。

volatile的有序性

有序性是指程序代碼的執(zhí)行是按照代碼的實現順序來按序執(zhí)行的。

volatile的有序性特性則是指禁止JVM指令重排優(yōu)化。

我們來看一個例子:

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;
 }
}

上面的代碼是一個很常見的單例模式實現方式,但是上述代碼在多線程環(huán)境下是有問題的。為什么呢,問題出在instance對象的初始化上,因為 instance = new Singleton(); 這個初始化操作并不是原子的,在JVM上會對應下面的幾條指令:

memory =allocate(); //1. 分配對象的內存空間 
ctorInstance(memory); //2. 初始化對象 
instance =memory;  //3. 設置instance指向剛分配的內存地址

上面三個指令中,步驟2依賴步驟1,但是步驟3不依賴步驟2,所以JVM可能針對他們進行指令重拍序優(yōu)化,重排后的指令如下:

memory =allocate(); //1. 分配對象的內存空間 
instance =memory;  //3. 設置instance指向剛分配的內存地址
ctorInstance(memory); //2. 初始化對象

這樣優(yōu)化之后,內存的初始化被放到了instance分配內存地址的后面,這樣的話當線程1執(zhí)行步驟3這段賦值指令后,剛好有另外一個線程2進入getInstance方法判斷instance不為null,這個時候線程2拿到的instance對應的內存其實還未初始化,這個時候拿去使用就會導致出錯。

所以我們在用這種方式實現單例模式時,會使用volatile關鍵字修飾instance變量,這是因為volatile關鍵字除了可以保證變量可見性之外,還具有防止指令重排序的作用。當用volatile修飾instance之后,JVM執(zhí)行時就不會對上面提到的初始化指令進行重排序優(yōu)化,這樣也就不會出現多線程安全問題了。

volatile使用場景

volatile的可以在以下場景中使用:

當運算結果不依賴變量當前的值,或者能確保只有單一線程修改變量的值的時候,我們才可以對該變量使用volatile關鍵字
變量不需要與其他狀態(tài)變量共同參與不變約束

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);
 }
}

上面這段代碼創(chuàng)建了20個線程,每個線程對變量count進行1000次自增操作,如果這段代碼并發(fā)正常的話,結果應該是20000,但實際運行過程中經常會出現小于20000的結果,因為count++這個自增操作不是原子操作。

上面的count++自增操作等價于count=count+1,所以JVM需要先讀取count的值,然后在count的基礎上給它加1,然后再將新的值重新賦值給count變量,所以這個自增總共需要三步。

 Java多線程之volatile關鍵字及內存屏障實例解析

上圖中我將線程對count的自增操作畫了個簡單的流程,一個線程要對count進行自增時要先讀取count的值,然后在當前count值的基礎上進行count+1操作,最后將count的新值重新寫回到count。

如果線程2在線程1讀取count舊值寫回count新值期間讀取count的值,顯然這個時候線程2讀取的是count還未更新的舊值,這時兩個線程是對同一個值進行了+1操作,這樣這兩個線程就沒有對count實現累加效果,相反這些操作卻又沒有違反volatile的定義,所以這種情況下使用volatile依然會存在多線程并發(fā)安全的問題。

volatile與內存屏障

前面介紹了volatile的可見性和有序性,那JVM到底是如何為volatile關鍵字實現的這兩大特性呢,Java內存模型其實是通過內存屏障(Memory Barrier)來實現的。

內存屏障其實也是一種JVM指令,Java內存模型的重排規(guī)則會要求Java編譯器在生成JVM指令時插入特定的內存屏障指令,通過這些內存屏障指令來禁止特定的指令重排序。

另外內存屏障還具有一定的語義:內存屏障之前的所有寫操作都要回寫到主內存,內存屏障之后的所有讀操作都能獲得內存屏障之前的所有寫操作的最新結果(實現了可見性)。因此重排序時,不允許把內存屏障之后的指令重排序到內存屏障之前。

下面的表是volatile有關的禁止指令重排的行為:

第一個操作 第二個操作:普通讀寫 第二個操作:volatile讀 第二個操作:volatile寫
普通讀寫 可以重排 可以重排 不可以重排
volatile讀 不可以重排 不可以重排 不可以重排
volatile寫 可以重排 不可以重排 不可以重排

從上面的表我們可以得出下面這些結論:

當第二個操作volatile寫時,不論第一個操作是什么,都不能重排序。這個規(guī)則保證了volatile寫之前的操作不會被重排到volatile寫之后。

當第一個操作為volatile讀時,不論第二個操作是什么,都不能重排。這個操作保證了volatile讀之后的操作不會被重排到volatile讀之前。

當第一個操作為volatile寫,第二個操作為volatile讀時,不能重排。

JVM中提供了四類內存屏障指令:

屏障類型 指令示例 說明
LoadLoad Load1; LoadLoad; Load2 保證load1的讀取操作在load2及后續(xù)讀取操作之前執(zhí)行
StoreStore Store1; StoreStore; Store2 在store2及其后的寫操作執(zhí)行前,保證store1的寫操作已刷新到主內存
LoadStore Load1; LoadStore; Store2 在stroe2及其后的寫操作執(zhí)行前,保證load1的讀操作已讀取結束
StoreLoad Store1; StoreLoad; Load2 保證store1的寫操作已刷新到主內存之后,load2及其后的讀操作才能執(zhí)行

總結

volatile實現了Java內存模型中的可見性和有序性,它的這兩大特性則是通過內存屏障來實現的,同時volatile無法保證原子性。

以上所述是小編給大家介紹的Java多線程之volatile關鍵字及內存屏障實例解析,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI