詳解Java volatile 內存屏障底層原理語義
一、volatile關鍵字介紹及底層原理
1.volatile的特性(內存語義)
當一個變量被定義成volatile之后,它將具備兩項特性:第一項是保證此變量對所有線程的可見性,這里的“可見性”是指當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的。而普通變量并不能做到這一點,普通變量的值在線程間傳遞時均需要通過主內存來完成。比如,線程A修改一個普通變量的值,然后向主內存進行回寫,另外一條線程B在線程A回寫完成了之后再對主內存進行讀取操作,新變量值才會對線程B可見。
使用volatile變量的第二個語義是禁止指令重排序優(yōu)化,普通的變量僅會保證在該方法的執(zhí)行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。因為在同一個線程的方法執(zhí)行過程中無法感知到這點,這就是Java內存模型中描述的所謂“線程內表現(xiàn)為串行的語義”(Within-Thread As-If-Serial Semantics)。
2.volatile底層原理
volatile關鍵字修飾的變量可以保證可見性與有序性,無法保證原子性。那么volatile關鍵字的底層原理是什么呢?我們可以通過查看Java代碼的匯編指令去看一下volatile的底層原理:查詢Java代碼的匯編指令需要設置JVM允許參數(shù):-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp;如果你的jdk版本小于等于8還要在jdk里面添加Hsdis插件,將該插件目錄里面的兩個文件(hsdis-amd64.dll,hsdis-i386.dll)復制到 %JAVA_HOME%\jre\bin\server 下,然后運行你的Java程序,就可以看到控制臺里面一堆的匯編指令代碼輸出了。
public class Singleton {
private volatile static Singleton myinstance;
public static Singleton getInstance() {
if (myinstance == null) {
synchronized (Singleton.class) {
if (myinstance == null) {
myinstance = new Singleton();//對象創(chuàng)建過程,本質可以分文三步
}
}
}
return myinstance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
上面所示是一段標準的雙鎖檢測(Double Check Lock,DCL)單例代碼,可以觀察加入volatile和未加入volatile關鍵字時所生成的匯編代碼的差別。不加volatile關鍵字時在控制臺輸出指令搜索myinstance可以看到如下兩行
0x00000000038064dd: mov %r10d,0x68(%rsi)
0x00000000038064e1: shr $0x9,%rsi
0x00000000038064e5: movabs $0xf1d8000,%rax
0x00000000038064ef: movb $0x0,(%rsi,%rax,1) ;*putstatic myinstance
; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)
加了volatile關鍵字后,變成下面這樣了:
0x0000000003cd6edd: mov %r10d,0x68(%rsi)
0x0000000003cd6ee1: shr $0x9,%rsi
0x0000000003cd6ee5: movabs $0xf698000,%rax
0x0000000003cd6eef: movb $0x0,(%rsi,%rax,1)
0x0000000003cd6ef3: lock addl $0x0,(%rsp) ;*putstatic myinstance
; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)
通過對比發(fā)現(xiàn),關鍵變化在于有volatile修飾的變量,賦值后(前面movb $0x0,(%rsi,%rax,1)這句便是賦值操作)多執(zhí)行了一個“l(fā)ock addl $0x0,(%rsp)”操作,這個操作的作用相當于一個內存屏障(Memory Barrier或Memory Fence,指重排序時不能把后面的指令重排序到內存屏障之前的位置,只有一個處理器訪問內存時,并不需要內存屏障;但如果有兩個或更多處理器訪問同一塊內存,且其中有一個在觀測另一個,就需要內存屏障來保證一致性了。
這里的關鍵在于lock前綴,它的作用是將本處理器的緩存寫入了內存,該寫入動作也會引起別的處理器或者別的內核無效化(Invalidate,MESI協(xié)議的I狀態(tài))其緩存,這種操作相當于對緩存中的變量做了一次前面介紹Java內存模式中所說的“store和write”操作。所以通過這樣一個操作,可讓前面volatile變量的修改對其他處理器立即可見。lock指令的更底層實現(xiàn):如果支持緩存行會加緩存鎖(MESI);如果不支持緩存鎖,會加總線鎖。
二、volatile——可見性
volatile修飾變量之后,可以保證可見性,下面通過一個程序示例演示一下:
public class VolatileVisibilitySample {
private volatile boolean initFlag = false;
static Object object = new Object();
public void refresh(){
this.initFlag = true;
System.out.println("線程:"+Thread.currentThread().getName()+":修改共享變量initFlag");
}
public void load(){
int i = 0;
while (!initFlag){
// synchronized (object){
// i++;
// }
}
System.out.println("線程:"+Thread.currentThread().getName()+"當前線程嗅探到initFlag的狀態(tài)的改變"+i);
}
public static void main(String[] args) throws InterruptedException {
VolatileVisibilitySample sample = new VolatileVisibilitySample();
Thread threadA = new Thread(()->{
sample.refresh();
},"threadA");
Thread threadB = new Thread(()->{
sample.load();
},"threadB");
threadB.start();
Thread.sleep(2000);
threadA.start();
}
}
可以看到共享變量被volatile修飾之前,線程B中調用的方法中 “當前線程嗅探到initFlag的狀態(tài)的改變” 這句輸出是打印不出來的,也就意味著線程A中將initFlag改為true,但是線程B并沒有獲取到最新值,程序一直在循環(huán)空跑。此時JMM操作如下圖:雖然線程A中將initFlag改為了true并且最終會同步回主內存,但是線程B中循環(huán)讀取的initFlag一直都是從工作內存讀取的,所以會一直進行死循環(huán)無法退出。

添加了volatile修飾之后,“當前線程嗅探到initFlag的狀態(tài)的改變” 這句話就會被打印出來,因為添加volatile關鍵字后,就會有l(wèi)ock指令,使用緩存一致性協(xié)議,線程B中會一直嗅探initFlag是否被改變,線程A修改initFlag后會立即同步回主內存,這時候會通知線程B將緩存行狀態(tài)改為I(無效狀態(tài)),需要重新從主內存讀取。如下圖所示:

我們將上面的代碼的load()方法進行修改——去掉volatile關鍵字,添加synchronized同步塊,即修改為下面這樣的情況,會達到跟添加volatile關鍵字相同的效果,這是因為添加了鎖同步塊,CPU會分配時間片,線程進行鎖競爭導致線程上下文切換,重新讀取主存的變量。
public void load(){
int i = 0;
while (!initFlag){
synchronized (object){
i++;
}
}
System.out.println("線程:"+Thread.currentThread().getName()+"當前線程嗅探到initFlag的狀態(tài)的改變"+i);
}
三、volatile——無法保證原子性
由于volatile變量只能保證可見性,在不符合以下兩條規(guī)則的運算場景中,我們仍然要通過加鎖(使用synchronized、java.util.concurrent中的鎖或原子類)來保證原子性:
- 運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
- 變量不需要與其他的狀態(tài)變量共同參與不變約束
下面通過一個示例演示一下:10個線程,每個線程加1000次(counter++不是一個原子性的操作,可以通過javap命令查看底層指令,可以看到有加載變量數(shù)據、將變量放到操作數(shù)棧頂、執(zhí)行加法運算等操作)。運行幾次發(fā)現(xiàn),有時運行結果是小于10000的。下面分析一下:
- 1.首先counter不加volatile修飾時:因為10個線程同時對變量進行自加1運算,每個運算一次后去寫會主內存,會覆蓋其他線程的運算結果,所以運行結果可能會小于10000。
- 2.counter添加volatile修飾時:添加volatile修飾之后,變量被修改后會立即同步回主存,一直嗅探其他線程是否對變量進行過修改,修改后重新從主存讀取變量。但是正因為添加了volatile關鍵字時MESI緩存一致性協(xié)議生效了,當一個變量執(zhí)行加1操作后,需要同步回主存,這是會鎖緩存行,通知其他線程變量已經被修改過了,將本地緩存行改為I無效狀態(tài),這樣被改為無效狀態(tài)的線程本地加1操作的結果被丟棄了,沒有寫回主內存,也就是白加了一次,所以運行結果也可能會小于10000。
想要實現(xiàn)原子性操作,可以通過synchronized,ReentrantLock加鎖,或者使用AtomicInteger進行原子性運算。
public class VolatileAtomicSample {
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(()->{
for (int j = 0; j < 1000; j++) {
counter++;
}
});
thread.start();
}
Thread.sleep(1000);
System.out.println(counter);
}
}
四、volatile——禁止指令重排
1.指令重排
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。java語言規(guī)范規(guī)定JVM線程內部維持順序化語義。即只要程序的最終結果與
它順序化情況的結果相等,那么指令的執(zhí)行順序可以與代碼順序不一致,此過程叫指令的重排序。指令重排序的意義是什么?JVM能根據處理器特性(CPU多級緩存系統(tǒng)、多核處理器等)適當?shù)膶C器指令進行重排序,使機器指令能更符合CPU的執(zhí)行特性,最大限度的發(fā)揮機器性能。
下圖為從源碼到最終執(zhí)行的指令序列示意圖

指令重排主要有兩個階段:
1.編譯器編譯階段:編譯器加載class文件編譯為機器碼時進行指令重排
2.CPU執(zhí)行階段: CPU執(zhí)行匯編指令時,可能會對指令進行重排序
2.as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據依賴關系的操作做重排序,因為這種重排序會改變執(zhí)行結果。但是,如果操作之間不存在數(shù)據依賴關系,這些操作就可能被編譯器和處理器重排序。
通過一個程序代碼,演示一下指令重排的效果:只有x=0并且y=0的情況下才會跳出循環(huán)
public class VolatileReOrderSample {
private static int x = 0, y = 0;
private static int a = 0, b =0;
static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (;;){
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
通過分析,會有三種可能的輸出:[0,1],[1,0],[1,1]。
- 輸出可能1——[0,1]:線程1先執(zhí)行完,線程2再執(zhí)行,則會出現(xiàn)x=0,y=1
- 輸出可能1——[1,0]:線程2先執(zhí)行完,線程1再執(zhí)行,則會出現(xiàn)x=1,y=0
- 輸出可能1——[1,1]:線程1、線程2交替執(zhí)行,a=1,b=1,然后執(zhí)行x=1,y=1,則會出現(xiàn)x=1,y=1
當運行之后會發(fā)現(xiàn)上面分析的三種情況確實出現(xiàn)了,但是程序最終跳出了循環(huán),也就是出現(xiàn)了x=0并且y=0的情況,這說明出現(xiàn)了指令重排的情況,即線程1中a=1 x=b的指令出現(xiàn)了順序調整或線程2中b=1 y=a的指令出現(xiàn)了順序調整。
當我們給變量a和b添加volatile關鍵字修飾后(private volatile static int a = 0, b =0;),再次運行發(fā)現(xiàn)程序一直在循環(huán)輸出,沒有出現(xiàn)x=y=0的情況從而退出循環(huán)。
volatile可以禁止指令重排的原因是因為添加了lock指令,會添加內存屏障。
五、volatile與內存屏障(Memory Barrier)
1.內存屏障(Memory Barrier)
內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的作用有兩個,一是保證特定操作的執(zhí)行順序,二是保證某些變量的內存可見性(利用該特性實現(xiàn)volatile的內存可見性)。由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏障禁止在內存屏障前后的指令執(zhí)行重排序優(yōu)化。Memory Barrier的另外一個作用是強制刷出各種CPU的緩存數(shù)據,因此任何CPU上的線程都能讀取到這些數(shù)據的最新版本??傊瑅olatile變量正是通過內存屏障(lock指令)實現(xiàn)其在內存中的語義,即可見性和禁止重排優(yōu)化。
上面的程序示例:synchronized+volatile實現(xiàn)的DCL模式的單例模式,就是利用了volatile禁止指令重排的特性。因為myinstance = new Singleton();這句代碼本質上是有三步:1.為對象分配內存空間;2.實例化對象數(shù)據;3.將引用指向對象實例的內存空間。如果第一個線程執(zhí)行創(chuàng)建對象時出現(xiàn)了指令重排,比如3排到了2之前,那么線程2在最外層代碼判斷myinstance!=null為true返回對象引用,但是實際上這時候對象尚未初始化完成,這樣是有問題的,需要通過添加volatile關鍵字去禁止指令重排。
2.volatile的內存語義實現(xiàn)
前面提到過重排序分為編譯器重排序和處理器重排序。為了實現(xiàn)volatile內存語義,JMM會分別限制這兩種類型的重排序類型。下圖是JMM針對編譯器制定的volatile重排序規(guī)則表。

舉例來說,第三行最后一個單元格的意思是:在程序中,當?shù)谝粋€操作為普通變量的讀或寫時,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作。
從上圖我們可以看出:
- 當?shù)诙€操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規(guī)則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
- 當?shù)谝粋€操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規(guī)則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
- 當?shù)谝粋€操作是volatile寫,第二個操作是volatile讀時,不能重排序。
為了實現(xiàn)volatile的內存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內存屏障插入策略。
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
上述內存屏障插入策略非常保守,但它可以保證在任意處理器平臺,任意的程序中都能得到正確的volatile內存語義。
下面是保守策略下,volatile寫插入內存屏障后生成的指令序列示意圖,如圖所示。

上圖中StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經對任意處理器可見了。這是因為StoreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內存。
而volatile寫后面的StoreLoad屏障,作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序
下圖是在保守策略下,volatile讀插入內存屏障后生成的指令序列示意圖

上圖中LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
上述volatile寫和volatile讀的內存屏障插入策略非常保守。在實際執(zhí)行時,只要不改變 volatile寫-讀的內存語義,編譯器可以根據具體情況省略不必要的屏障。
六、JMM對volatile的特殊規(guī)則定義
最后我們再Java內存模型中對volatile變量定義的特殊規(guī)則的定義。假定T表示一個線程,V和W分別表示兩個volatile型變量,那么在進行read、load、use、assign、store和write操作時需要滿足如下規(guī)則:
只有當線程T對變量V執(zhí)行的前一個動作是load的時候,線程T才能對變量V執(zhí)行use動作;并且,只有當線程T對變量V執(zhí)行的后一個動作是use的時候,線程T才能對變量V執(zhí)行l(wèi)oad動作。線程T對變量V的use動作可以認為是和線程T對變量V的load、read動作相關聯(lián)的,必須連續(xù)且一起出現(xiàn)。
這條規(guī)則要求在工作內存中,每次使用V前都必須先從主內存刷新最新的值,用于保證能看見其他線程對變量V所做的修改。
只有當線程T對變量V執(zhí)行的前一個動作是assign的時候,線程T才能對變量V執(zhí)行store動作;并且,只有當線程T對變量V執(zhí)行的后一個動作是store的時候,線程T才能對變量V執(zhí)行assign動作。線程T對變量V的assign動作可以認為是和線程T對變量V的store、write動作相關聯(lián)的,必須連續(xù)且一起出現(xiàn)。
這條規(guī)則要求在工作內存中,每次修改V后都必須立刻同步回主內存中,用于保證其他線程可以看到自己對變量V所做的修改。
假定動作A是線程T對變量V實施的use或assign動作,假定動作F是和動作A相關聯(lián)的load或store動作,假定動作P是和動作F相應的對變量V的read或write動作;與此類似,假定動作B是線程T對變量W實施的use或assign動作,假定動作G是和動作B相關聯(lián)的load或store動作,假定動作Q是和動作G相應的對變量W的read或write動作。如果A先于B,那么P先于Q。
這條規(guī)則要求volatile修飾的變量不會被指令重排序優(yōu)化,從而保證代碼的執(zhí)行順序與程序的順序相同。
下一篇預告——并發(fā)編程三大特性:原子性,可見性,有序性,happen-before原則
到此這篇關于詳解Java volatile 內存屏障底層原理語義的文章就介紹到這了,更多相關Java volatile 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JAVA演示阿里云圖像識別API,印刷文字識別-營業(yè)執(zhí)照識別
最近有由于工作需要,開始接觸阿里云的云市場的印刷文字識別API-營業(yè)執(zhí)照識別這里我加上了官網的申請說明,只要你有阿里云賬號就可以用,前500次是免費的,API說明很簡陋,只能做個簡單參考2019-05-05
java高并發(fā)下解決AtomicLong性能瓶頸方案LongAdder
這篇文章主要為大家介紹了java高并發(fā)下解決AtomicLong性能瓶頸方案LongAdder,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
Spring?web開發(fā)教程之Request獲取3種方式
這篇文章主要給大家介紹了關于Spring?web開發(fā)教程之Request獲取3種方式的相關資料,request對象是從客戶端向服務器發(fā)出請求,包括用戶提交的信息以及客戶端的一些信息,需要的朋友可以參考下2023-11-11
解決idea報錯 Connot resolve column 的問題
這篇文章主要介紹了解決idea報錯 Connot resolve column 的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
Spring Boot jar 啟動時設置環(huán)境參數(shù)的操作
這篇文章主要介紹了Spring Boot jar 啟動時設置環(huán)境參數(shù)的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06

