學會了volatile,你變心了,我看到了

更多精彩文章,請關注xhJaver,京東工程師和你一塊兒成長

volatile 簡介

通常用來修飾共享變量,保證可見性和能夠禁止指令重排java

  • 多線程操做同一個變量的時候,某一個線程修改完,其餘線程能夠當即看到修改的值,保證了共享變量的可見性
  • 禁止指令重排,保證了代碼執行的有序性
  • 不保證原子性,例如常見的i++

    (可是對單次讀或者寫保證原子性)mysql

可見性代碼示例

如下代碼建議使用PC端來查看,複製黏貼直接運行,都有詳細註釋

咱們來寫個代碼測試一下,多線程修改共享變量時究竟需不須要用volatile修飾變量面試

  1. 首先,咱們建立一個任務類
public class Task implements Runnable{
 @Override
 public void run() {
 System.out.println("這是"+Thread.currentThread().getName()+"線程開始,flag是 "+Demo.flag);
 //當共享變量是true時,就一直卡在這裏,不輸出下面那句話
 // 當flag是false時,輸出下面這句話
 while (Demo.flag){
 }
 System.out.println("這是"+Thread.currentThread().getName()+"線程結束,flag是 "+Demo.flag);
 }
}

2.其次,咱們建立個測試類sql

class Demo {
 //共享變量,還沒用volatile修飾
 public static   boolean flag = true ;
 public static void main(String[] args) throws InterruptedException {
 System.out.println("這是"+Thread.currentThread().getName()+"線程開始,flag是 "+flag);
 //開啓剛纔線程
 new Thread(new Task()).start();
 try {
 //沉睡一秒,確保剛纔的線程已經跑到了while循環
 //要否則還沒跑到while循環,主線程就將flag變爲false
 Thread.sleep(1000L);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 //改變共享變量flag轉爲false
 flag = false;
 System.out.println("這是"+Thread.currentThread().getName()+"線程結束,flag是 "+flag);
 }
}

3.咱們查看一下輸出結果緩存

可見,程序並無結束,他卡在了這裏,爲何卡在了這裏呢,就是由於咱們在主線程修改了共享變量flag爲false,可是另外一個線程沒有感知到,這個變量的修改對另外一個線程不可見多線程

  • 若是要是用volatile變量修飾的話,結果就變成了下面這個樣子

public static volatile boolean flag = trueide

可見,此次主線程修改的變量被另外一個線程所感知到了,保證了變量的可見性測試

可見性原理分析

那麼,神奇的 volatile 底層到底作了什麼呢,你的改變,逃不過他的法眼?爲何不用他修飾變量的話,變量的改變其餘線程就看不見?優化

回答此問題的時候首先,咱們須要瞭解一下JMM(Java內存模型)this

注: 本地內存是JMM的一種抽象,並非真實存在的,本地內存它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化以後的一個數據存放位置
  • 由此咱們能夠分析出來,主線程修改了變量,可是其餘線程不知道,有兩種狀況

    1. 主線程修改的變量尚未來得及刷新到主內存中,另外一個線程讀取的仍是之前的變量
    2. 主線程修改的變量刷新到了主內存中,可是其餘線程讀取的仍是本地的副本
  • 當咱們用 volatile 關鍵字修飾共享變量時就能夠作到如下兩點

    1. 當線程修改變量時,會強制刷新到主內存中
    2. 當線程讀取變量時,會強制從主內存讀取變量而且刷新到工做內存中

指令重排

  • 何爲指令重排?

爲了提升程序運行效率,編譯器和cpu會對代碼執行的順序進行重排列,可這有時候會帶來不少問題

咱們來看下代碼

//指令重排測試
public class Demo2 {
private Integer number = 10;
private boolean flag = false;
private Integer result = 0;
public void  write(){
this.flag = true; // L1
this.number = 20; // L2
}
public void  reader(){
while (this.flag){ // L3
this.result = this.number + 1; // L4
}
}
}

假如說咱們有A、B兩個線程 他們分別執行write()方法和 reader()方法,執行的順序有可能以下圖所示

  • 問題分析: 如圖可見,A線程的L2和L1的執行順序重排序了,若是要是這樣執行的話,當A執行完L2時,B開始執行L3,但是這個時候flag仍是爲false,那麼L4就執行不了了,因此result的值仍是初始值0,沒有被改變爲21,致使程序執行錯誤

這個時候,咱們就能夠用volatile關鍵字來解決這個問題,很簡單,只需

private volatile Integer number = 10;

  • 這個時候L1就必定在L2前面執行
A線程在修改 number變量爲20的時候,就確保這句代碼的前面的代碼必定在此行代碼以前執行,在 number處插入了 內存屏障 ,爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排

內存屏障

內存屏障又是什麼呢?一共有四種內存屏障類型,他們分別是

  1. LoadLoad屏障:

    • Load1 LoadLoad Load2 確保Load1的數據的裝載先於Load2及全部後續裝載指令的裝載
  2. LoadStore屏障:

    • Load1 LoadStore Store2 確保Load1的數據的裝載先於Store2及全部後續存儲指令的存儲
  3. StoreLoad屏障:

    • Store1 StoreLoad Load2 確保Store1的數據對其餘處理器可見(刷新到內存)先於Load2及全部後續的裝載指令的裝載
  4. StoreStore屏障:

    • Store1 StoreStore Store2 確保Store1數據對其餘處理器可見(刷新到內存)先於Store2及全部後續存儲指令的存儲
> StoreLoad 是一個全能型的屏障,同時具備其餘3個屏障的效果。執行該屏障的花銷比較昂貴,由於處理器一般要把當前的寫緩衝區的內容所有刷新到內存中(Buffer Fully Flush)
  • 裝載load 就是讀 int a = load1 ( load1的裝載)
  • 存儲store就是寫 store1 = 5 ( store1的存儲)

volatile與內存屏障

那麼volatile和這四種內存屏障又有什麼關係呢,具體是怎麼插入的呢?

  1. volatile寫 (先後都插入屏障)

    • 前面插入一個StoreStore屏障
    • 後面插入一個StoreLoad屏障
  2. volatile讀(只在後面插入屏障)

    • 後面插入一個LoadLoad屏障
    • 後面插入一個LoadStore屏障

官方提供的表格是這樣的

咱們此時回過頭來在看咱們的那個程序

this.flag = true; // L1
this.number = 20; // L2

因爲number被volatile修飾了,L2這句話是volatile寫,那麼加入屏障後就應該是這個樣子

this.flag = true; // L1
//  StoreStore  確保flag數據對其餘處理器可見(刷新到內存)先於number及全部後續存儲指令的存儲
this.number = 20; // L2
// StoreLoad  確保number數據對其餘處理器可見(刷新到內存)先於全部後續存儲指令的裝載

因此L1,L2的執行順序不被重排序

ps:總部四號樓真是愈來愈好了,獎勵本身一杯奶茶

更多精彩,請關注公衆號xhJaver,京東工程師和你一塊兒成長

往期精彩

相關文章
相關標籤/搜索