三面阿里竟然敗在了volatile關鍵字上
今天閑來無事跟同事?大帆?閑聊,
SoWhat:麥叔聽說你偷偷面阿里啦,面的咋樣?
大帆:一面挺簡單的,主要問了一些基本的數據結構跟算法,還問了下??HashMap的十大常見基本問題。我都答案上來了,還問了我JDK7環(huán),幸虧你那個?HashMap環(huán)?繪制的牛逼,我答的不錯就讓我準備二面了。
SoWhat:二面類?
大帆:二面問了我一些JVM的問題,問我對于JVM內存模型的理解,還有GC的常見理解,最終還問了我下類加載機制,我看你之前水過這個 JVM系列,就依葫蘆畫瓢答上來了,讓我準備三面。
SoWhat:大帆這波可以啊,三面問的啥???
大帆:三面問了我一些CAS、Lock、AQS跟 ConcurrentHashMap 的底層實現什么的,還問了我下?線程池?的七大參數跟四大拒絕策略,以及使用注意事項。我看你水過?并發(fā)編程系列,也就答上來了。
Sowhat:厲害啊這是要過的節(jié)奏阿!
大帆:過個錘子,三面的這個總監(jiān)最后竟然問了我下我對volatile的底層原理。你妹的你么水,我就答了一些基本的可見性跟弱原子性,然后我感覺面試官不太滿意?。?/p>
Sowhat:額好吧,那我抓緊再水文寫下個關于volatile的使用。
使用
volatile變量自身具有下列特性相信大家都知道:
??
可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復合操作不具有原子性。
其中第二點可以理解為把對?volatile?變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步,就跟下面的SoWhat跟SynSoWhat功能類似哦。
class?SoWhat{
????volatile?int?i?=?0;?//?volatile修飾的變量
????public?int?getI(){
????????return?i;//?單個volatile變量的讀
????}
????public??void?setI(int?j){
????????this.i?=?j;?//?單個volatile?變量的寫
????}
????public?void?inc(){
????????i++;//復合多個volatile?變量
????}
}
class?SynSoWhat{
?????int?i?=?0;
????public?synchronized?int?getI(){
????????return?i;
????}
????public??synchronized?void?setI(int?j){
????????this.i?=?j;
????}
????public?void?inc(){?//?普通方法調用
????????int?tmp?=?getI();?//?調用已同步方法
????????tmp?=?tmp?+?1;//普通寫方法
????????setI(tmp);//?調用已同步方法
????}
}
寫理解
volatile寫的內存語義如下:
?當寫一個
?volatile變量時,JMM會把該線程對應的本地中的共享變量值刷新到主內存。
public?class?VolaSemanteme?{
?int?a?=?0;
?volatile?boolean?flag?=?false;?//?這是重點哦
?public?void?init()?{
??a?=?1;?
??flag?=?true;?
??//.......
?}
?public?void?use()?{
??if?(flag)?{?
???int?i?=?a?*?a;?
??}
??//.......
?}
}
線程A調用init方法,線程B調用use方法。
讀理解
volatile讀的內存語義如下:
?當讀一個volatile變量時,JMM會把該線程對應的本地內存置為
?無效。線程接下來將從主內存中讀取共享變量。
public?class?VolaSemanteme?{
?int?a?=?0;
?volatile?boolean?flag?=?false;?//?這是重點哦
?public?void?init()?{
??a?=?1;?
??flag?=?true;?
??//.......
?}
?public?void?use()?{
??if?(flag)?{?
???int?i?=?a?*?a;?
??}
??//.......
?}
}
流程圖大致是這樣的:
volatile 指令重排
volatile?變量的內存可見性是基于內存屏障(Memory Barrier)實現。關于內存屏障的具體講解以前寫過不再重復,JMM裝逼于無形這里說過。總結來說就是JMM內部會有指令重排,并且會有af-if-serial跟happen-before的理念來保證指令重拍的正確性。內存屏障就是基于4個匯編級別的關鍵字來禁止指令重排的,其中volatile的重拍規(guī)則如下:
??
第一個為讀操作時,第二個任何操作不可重排序到第一個前面。 第二個為寫操作時,第一個任何操作不可重排序到第二個后面。 第一個為寫操作時,第二個的讀寫操作也不運行重排序。

volatile寫底層實現
JMM對volatile的內存屏障插入策略
?在每個volatile寫操作的前面插入一個StoreStore屏障。在每個volatile寫操作的后面插入一個StoreLoad屏障。
?

volatile 讀底層
JMM對volatile的內存屏障插入策略
?在每個volatile讀操作的后面插入一個LoadLoad屏障。在每個volatile讀操作的后面插入一個LoadStore屏障。
?

其中重點說下volatile讀后面為什么跟了個LoadLoad。加入我有如下代碼 AB兩個線程執(zhí)行,B線程的flag獲取下面的讀被提前了。

volatile的實現原理
有volatile變量修飾的共享變量進行寫操作的時候會使用CPU提供的Lock前綴指令。在CPU級別的功能如下:
??
將當前處理器緩存行的數據寫回到「系統內存」 這個寫回內存的操作會告知在其他CPU你們拿到的變量是無效的下一次使用時候要重新共享內存拿。
我們可以通過jitwatch對簡單的代碼進行詳細的反匯編看一下。
package?com.sowhat.demo;
public?class?VolaSemanteme?{
????int?unvloatileVal?=?0;
????volatile?boolean?flag?=?false;
????public?void?init()?{
????????unvloatileVal?=?1;
????????flag?=?true;?//?第九行哦
????}
????public?void?use()?{
????????if?(flag)?{
????????????int?LocalA?=?unvloatileVal;
????????????if?(LocalA?==?0)?{
????????????????throw?new?RuntimeException("error");
????????????}
????????}
????}
????public?static?void?main(String[]?args)?{
????????VolaSemanteme?volaSemanteme?=?new?VolaSemanteme();
????????volaSemanteme.init();
????????volaSemanteme.use();
????}
}
對普通變量的賦值操作:
對volatile變量的賦值操作。
可以對比得出,volatile 修飾的變量確實會多一個 lock addl $0x0,(%rsp) 指令。
0x0000000114ce95cb:?lock?addl?$0x0,(%rsp)??;*putfield?flag
??;?-?com.sowhat.demo.VolaSemanteme::init@7?(line?9)