學(xué)會了volatile,你變心了,我看到了
更多精彩文章,請關(guān)注xhJaver,京東工程師和你一起成長
volatile 簡介
一般用來修飾共享變量,保證可見性和可以禁止指令重排
多線程操作同一個變量的時候,某一個線程修改完,其他線程可以立即看到修改的值,保證了共享變量的可見性
禁止指令重排,保證了代碼執(zhí)行的有序性
不保證原子性,例如常見的i++
(但是對單次讀或者寫保證原子性)
可見性代碼示例
以下代碼建議使用PC端來查看,復(fù)制黏貼直接運行,都有詳細(xì)注釋
我們來寫個代碼測試一下,多線程修改共享變量時究竟需不需要用volatile修飾變量
首先,我們創(chuàng)建一個任務(wù)類
public class Task implements Runnable{
@Override
public void run() {
System.out.println("這是"+Thread.currentThread().getName()+"線程開始,flag是 "+Demo.flag);
//當(dāng)共享變量是true時,就一直卡在這里,不輸出下面那句話
// 當(dāng)flag是false時,輸出下面這句話
while (Demo.flag){
}
System.out.println("這是"+Thread.currentThread().getName()+"線程結(jié)束,flag是 "+Demo.flag);
}
}
2.其次,我們創(chuàng)建個測試類
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 {
//沉睡一秒,確保剛才的線程已經(jīng)跑到了while循環(huán)
//要不然還沒跑到while循環(huán),主線程就將flag變?yōu)閒alse
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//改變共享變量flag轉(zhuǎn)為false
flag = false;
System.out.println("這是"+Thread.currentThread().getName()+"線程結(jié)束,flag是 "+flag);
}
}
3.我們查看一下輸出結(jié)果

可見,程序并沒有結(jié)束,他卡在了這里,為什么卡在了這里呢,就是因為我們在主線程修改了共享變量flag為false,但是另一個線程沒有感知到,這個變量的修改對另一個線程不可見
如果要是用volatile變量修飾的話,結(jié)果就變成了下面這個樣子
public static volatile boolean flag = true

可見,這次主線程修改的變量被另一個線程所感知到了,保證了變量的可見性
可見性原理分析
那么,神奇的 volatile 底層到底做了什么呢,你的改變,逃不過他的法眼?為什么不用他修飾變量的話,變量的改變其他線程就看不見?
回答此問題的時候首先,我們需要了解一下JMM(Java內(nèi)存模型)

注:本地內(nèi)存是JMM的一種抽象,并不是真實存在的,本地內(nèi)存它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化之后的一個數(shù)據(jù)存放位置
由此我們可以分析出來,主線程修改了變量,但是其他線程不知道,有兩種情況
主線程修改的變量還沒有來得及刷新到主內(nèi)存中,另一個線程讀取的還是以前的變量 主線程修改的變量刷新到了主內(nèi)存中,但是其他線程讀取的還是本地的副本 當(dāng)我們用
volatile關(guān)鍵字修飾共享變量時就可以做到以下兩點當(dāng)線程修改變量時,會強制刷新到主內(nèi)存中 當(dāng)線程讀取變量時,會強制從主內(nèi)存讀取變量并且刷新到工作內(nèi)存中
指令重排
何為指令重排?
為了提高程序運行效率,編譯器和cpu會對代碼執(zhí)行的順序進(jìn)行重排列,可這有時候會帶來很多問題
我們來看下代碼
//指令重排測試
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兩個線程
他們分別執(zhí)行write()方法和 reader()方法,執(zhí)行的順序有可能如下圖所示
問題分析: 如圖可見,A線程的L2和L1的執(zhí)行順序重排序了,如果要是這樣執(zhí)行的話,當(dāng)A執(zhí)行完L2時,B開始執(zhí)行L3,可是這個時候flag還是為false,那么L4就執(zhí)行不了了,所以result的值還是初始值0,沒有被改變?yōu)?1,導(dǎo)致程序執(zhí)行錯誤
這個時候,我們就可以用volatile關(guān)鍵字來解決這個問題,很簡單,只需
private volatile Integer number = 10;
這個時候L1就一定在L2前面執(zhí)行
A線程在修改
number變量為20的時候,就確保這句代碼的前面的代碼一定在此行代碼之前執(zhí)行,在number處插入了內(nèi)存屏障 ,為了實現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排
內(nèi)存屏障
內(nèi)存屏障又是什么呢?一共有四種內(nèi)存屏障類型,他們分別是
LoadLoad屏障:
Load1 LoadLoad Load2 確保Load1的數(shù)據(jù)的裝載先于Load2及所有后續(xù)裝載指令的裝載 LoadStore屏障:
Load1 LoadStore Store2 確保Load1的數(shù)據(jù)的裝載先于Store2及所有后續(xù)存儲指令的存儲 StoreLoad屏障:
Store1 StoreLoad Load2 確保Store1的數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于Load2及所有后續(xù)的裝載指令的裝載 StoreStore屏障:
StoreLoad 是一個全能型的屏障,同時具有其他3個屏障的效果。執(zhí)行該屏障的花銷比較昂貴,因為處理器通常要把當(dāng)前的寫緩沖區(qū)的內(nèi)容全部刷新到內(nèi)存中(Buffer Fully Flush)
Store1 StoreStore Store2 確保Store1數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于Store2及所有后續(xù)存儲指令的存儲
裝載load 就是讀 int a = load1 ( load1的裝載) 存儲store就是寫 store1 = 5 ( store1的存儲)
volatile與內(nèi)存屏障
那么volatile和這四種內(nèi)存屏障又有什么關(guān)系呢,具體是怎么插入的呢?
volatile寫 (前后都插入屏障)
前面插入一個StoreStore屏障 后面插入一個StoreLoad屏障 
volatile讀(只在后面插入屏障)
后面插入一個LoadLoad屏障 后面插入一個LoadStore屏障 
官方提供的表格是這樣的
我們此時回過頭來再看我們的那個程序
this.flag = true; // L1
this.number = 20; // L2
由于number被volatile修飾了,L2這句話是volatile寫,那么加入屏障后就應(yīng)該是這個樣子
this.flag = true; // L1
// StoreStore 確保flag數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于number及所有后續(xù)存儲指令的存儲
this.number = 20; // L2
// StoreLoad 確保number數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于所有后續(xù)存儲指令的裝載
所以L1,L2的執(zhí)行順序不被重排序
ps:總部四號樓真是越來越好了,獎勵自己一杯奶茶

更多精彩,請關(guān)注公眾號xhJaver,京東工程師和你一起成長
