Volatile趣談——我是怎么把貝克漢姆的進(jìn)球弄丟的

大雄的門(mén)線傳感器

大雄的公司最近在給國(guó)際足球協(xié)會(huì)研制一款門(mén)線傳感器文狱。這可是一個(gè)大單盗棵,組織特地安排了大雄作為首席程序員泽铛,來(lái)開(kāi)發(fā)這款軟件尚辑。
需求很簡(jiǎn)單,傳感器需要在皮球越過(guò)門(mén)線的時(shí)候盔腔,給裁判身上的耳麥發(fā)送消息杠茬,告訴裁判球進(jìn)了。
“So easy”弛随,大雄四兩撥千斤地寫(xiě)了一個(gè)進(jìn)球通知線程:

public class GoalNotifier implements Runnable {
    public boolean goal = false;

    public boolean isGoal() {
        return goal;
    }

    public void setGoal(boolean goal) {
        this.goal = goal;
    }

    @Override
    public void run() {
        while (true) {
            if (isGoal()) {
                System.out.println("Goal !!!!!!");

                // Tell the referee the ball is in.
                // ...

                // reset goal flag
                setGoal(false);
            }
        }
    }
}

“只要在比賽一開(kāi)始就啟動(dòng)這個(gè)線程瓢喉,然后當(dāng)球越過(guò)球門(mén)線的時(shí)候,調(diào)用我的setGoal()方法舀透,把進(jìn)球標(biāo)志goal設(shè)置成true就OK了”栓票,大雄對(duì)著投影里的代碼,跟產(chǎn)品經(jīng)理胖虎講解著自己偉大的設(shè)計(jì)。
“很棒走贪!代碼寫(xiě)的非常簡(jiǎn)潔培他,設(shè)計(jì)非常優(yōu)雅捂刺,連我都看不出有什么Bug。靜香,不用浪費(fèi)時(shí)間測(cè)試了梯醒,直接上線吧户辱,時(shí)間就是金錢(qián)烫幕,我們要敢在別的競(jìng)爭(zhēng)對(duì)手之前济炎,推出這款產(chǎn)品!”凯亮,胖虎激動(dòng)的說(shuō)边臼,唾沫橫飛。
“好的假消,我也相信大雄的能力硼瓣!”,靜香含情脈脈的看著大雄置谦,眼里都是崇拜堂鲤。

Oop! Bug!

很快,大雄的門(mén)線傳感器上線了媒峡。英格蘭足協(xié)老總約翰是個(gè)很喜歡新科技的人瘟栖,他迫不及待地想把這項(xiàng)技術(shù)推廣到他的國(guó)家。
這天谅阿,有一場(chǎng)讓世界矚目的友誼賽——曼聯(lián)傳奇隊(duì) vs 阿森納傳奇隊(duì)半哟。“把我們剛剛買(mǎi)過(guò)來(lái)的門(mén)線技術(shù)用上去签餐,讓這群老家伙見(jiàn)識(shí)一下什么是高科技寓涨!”,約翰說(shuō)氯檐。

比賽開(kāi)始戒良,剛開(kāi)場(chǎng),只見(jiàn)魯尼把球往旁邊一撥冠摄,貝克漢姆就順勢(shì)一腳圓月彎刀糯崎,皮球劃出一道美麗的弧線,飛過(guò)大半個(gè)足球場(chǎng)河泳,阿森納門(mén)將始料不及沃呢,只能目送皮球應(yīng)聲入網(wǎng)!

這個(gè)過(guò)程之迅猛拆挥,只能用下面這段代碼來(lái)描述了:

public class Game {

    public static void main(String[] args) throws InterruptedException {
        // Game begun! Init goalNotifier thread
        GoalNotifier goalNotifier = new GoalNotifier();
        Thread goalNotifierThread = new Thread(goalNotifier);
        goalNotifierThread.start();

        // After 3s
        Thread.sleep(3000);
        // Goal !!!
        goalNotifier.setGoal(true);
    }

}

就在曼聯(lián)隊(duì)隊(duì)員抱在一起慶祝的時(shí)候薄霜,裁判跑了過(guò)來(lái),宣布進(jìn)球無(wú)效,原因是門(mén)線傳感器沒(méi)有提示他進(jìn)球了惰瓜。否副。。
“What ???”鸵熟,貝克漢姆一臉懵逼副编。负甸。流强。
“大雄,怎么回事呻待?打月??”蚕捉,約翰氣沖沖的對(duì)旁邊的大雄說(shuō)奏篙。
“啊,難道是線程沒(méi)起來(lái)嗎迫淹?”秘通,大雄也是一臉懵逼,“我加了日志的敛熬,看一下后臺(tái)就知道了肺稀!”
于是大雄登錄了后臺(tái)服務(wù)器,查看了日志信息:

“啊应民,一行日志都沒(méi)有话原。。诲锹》比剩”,大雄很慌归园,“看來(lái)只能求助哆啦了黄虱。。庸诱⌒”

大雄趕緊視頻了正在日本度假的哆啦,視頻里偶翅,哆啦一邊喝著大阪清酒默勾,一邊看著大雄的代碼,大概過(guò)了十秒鐘聚谁,突然掛斷了視頻母剥。
“難道連哆啦也沒(méi)有辦法了。』诽郏”习霹,就在大雄絕望的時(shí)候,他突然收到哆啦發(fā)來(lái)的信息炫隶,打開(kāi)一看淋叶,里面就一個(gè)詞:
volatile

“啊,難道是它伪阶。煞檩。≌ぬ”斟湃,來(lái)不及想太多了,貝克漢姆隨時(shí)都會(huì)再進(jìn)球檐薯,“不能讓我貝失望啊”凝赛,大雄趕緊改了一行代碼:

public class GoalNotifier implements Runnable {
//    public boolean goal = false;
    public volatile boolean goal = false;
    ...  

剛改完代碼,這邊曼聯(lián)隊(duì)就得到一個(gè)禁區(qū)外任意球的機(jī)會(huì)坛缕,貝克漢姆一記招牌的圓月彎刀墓猎,皮球直掛死角!不過(guò)這次赚楚,大家都沒(méi)慶祝毙沾,而是一致看向了裁判,全場(chǎng)鴉雀無(wú)聲直晨。搀军。。
突然勇皇,主席臺(tái)那里罩句,有一個(gè)像逗比一樣的青年,大聲的吼著敛摘,“Yeah!!! 日志打印出來(lái)了C爬谩!兄淫!”屯远,聲音之大,響徹全場(chǎng)捕虽。

過(guò)了大概兩秒鐘慨丐,人們才看到裁判把手指向了中圈,示意進(jìn)球有效泄私。房揭。备闲。

volatile和Java內(nèi)存模型

“為什么把goal變量加上volatile修飾符,問(wèn)題就解決了呢捅暴?”恬砂,帶著這個(gè)疑問(wèn),大雄開(kāi)始研究了起來(lái)蓬痒。漸漸的泻骤,他認(rèn)識(shí)到,看Java代碼梧奢,不能只看表象狱掂,還要透過(guò)Java虛擬機(jī),去看透本質(zhì)粹断。從JavaSE到JVM符欠,這是一場(chǎng)認(rèn)知的躍遷嫡霞。

首先要解決的問(wèn)題是瓶埋,不加volatile之前,main函數(shù)明明調(diào)用了setGoal()方法诊沪,把goal改成了true养筒,可為什么GoalNotifier線程里的goal還是false?
答案是端姚,主線程里調(diào)用setGoal()方法修改的goal晕粪,和GoalNotifier線程里的goal,是兩個(gè)副本渐裸。
What??? 變量還有副本巫湘?
單看代碼,自然是看不出“副本”的昏鹃,我們必須剝開(kāi)代碼這層皮尚氛,到Java虛擬機(jī)里頭去看看。

在介紹JVM中的“副本”之前洞渤,我們先來(lái)簡(jiǎn)單聊聊物理機(jī)的“副本”阅嘶,因?yàn)镴VM的副本和物理機(jī)的副本很像。
計(jì)算機(jī)载迄,相比于處理器的運(yùn)算速度讯柔,IO操作的速度往往有幾個(gè)數(shù)量級(jí)的差距,因此像下面這段常見(jiàn)的++運(yùn)算:

int count = 0;
...
count ++;

如果計(jì)算機(jī)把count的值存儲(chǔ)在內(nèi)存中护昧,那么每次++操作魂迄,就有一次從內(nèi)存中讀取i的值的操作,以及一次把i的值加1的操作惋耙,別忘了捣炬,還有一次把i的值寫(xiě)進(jìn)去內(nèi)存的操作:
T(一次循環(huán)) = T(讀IO) + T(+1運(yùn)算) + T(寫(xiě)IO)
而IO操作的速度往往比運(yùn)算速度多幾個(gè)數(shù)量級(jí)慈格,所以:
T(一次循環(huán)) ≈ T(讀IO) + T(寫(xiě)IO)
顯然,IO操作的速度嚴(yán)重拖后腿了遥金,不管運(yùn)算速度再快浴捆,只要IO操作還在,這個(gè)++操作的速度就永遠(yuǎn)由IO操作的速度決定稿械。
我們?nèi)祟?lèi)自然不允許這樣的情況發(fā)生选泻,因此我們?cè)谔幚砥骱蛢?nèi)存之間,引入了讀寫(xiě)速度接近處理器運(yùn)算速度的一層高速緩存

CPU美莫、高速緩存和內(nèi)存

這樣页眯,在上面的++操作里面,count變量只有在初始化的時(shí)候厢呵,需要寫(xiě)入主內(nèi)存窝撵,接著,count就被從主內(nèi)存拷貝到處理器的高速緩存中襟铭,下次再想對(duì)它執(zhí)行++操作時(shí)碌奉,直接從高速緩存中讀取就可以了,++操作執(zhí)行完之后寒砖,也不需要馬上同步到主內(nèi)存赐劣。

雖然各種平臺(tái)都會(huì)有高速緩存和主內(nèi)存,但是不同平臺(tái)的內(nèi)存模型并不完全相同哩都。這也就導(dǎo)致了像C/C++這種直接使用物理機(jī)內(nèi)存模型的編程語(yǔ)言魁兼,有時(shí)候一份代碼在一個(gè)平臺(tái)上可以正常運(yùn)行,去到另一個(gè)平臺(tái)就掛了漠嵌,所以需要“面向平臺(tái)”編程咐汞。而Java,正如廣告語(yǔ)說(shuō)的儒鹿,“Write once, run anywhere”化撕,相同的一份代碼,去到哪個(gè)平臺(tái)都可以直接拿過(guò)去用挺身。
為什么Java這么神奇呢侯谁?這自然是JVM的功勞,你下載JDK的時(shí)候章钾,會(huì)讓你選擇是Windows還是Linux的墙贱。使用不同平臺(tái)的JDK,最大的差異就是JVM了贱傀,相同的一份代碼惨撇,Windows版的JVM幫你把代碼翻譯成Windows系統(tǒng)能識(shí)別的機(jī)器語(yǔ)言,Linux版的JVM則翻譯成Linux的語(yǔ)言府寒。
JVM幫你屏蔽了不同平臺(tái)直接的差異魁衙。
自然的报腔,對(duì)于物理機(jī)的內(nèi)存模型,JVM也要進(jìn)行“介入”剖淀,我們編寫(xiě)的Java代碼纯蛾,是不會(huì)直接去操作物理機(jī)的內(nèi)存的,而是去操作JVM定義的Java內(nèi)存模型(Java Memory Model, JMM)纵隔,再通過(guò)JMM去操作物理機(jī)的內(nèi)存翻诉。
Java的內(nèi)存模型和上面講的物理機(jī)的內(nèi)存模型非常類(lèi)似:

Java內(nèi)存模型

現(xiàn)在再回過(guò)頭來(lái)看大雄碰到的問(wèn)題:main函數(shù)明明調(diào)用了setGoal()方法,把goal改成了true捌刮,可為什么GoalNotifier線程里的goal還是false碰煌?
答案已經(jīng)很明確了,這里面有兩個(gè)線程绅作,main函數(shù)所在的是主線程和GoalNotifier線程芦圾,這兩個(gè)線程都分別從主內(nèi)存從拷貝了一個(gè)goal變量的副本,所以當(dāng)main函數(shù)調(diào)用setGoal()方法修改goal時(shí)俄认,修改的其實(shí)是自己線程工作空間上的那個(gè)副本goal个少,對(duì)主內(nèi)存的goal沒(méi)有影響,對(duì)GoalNotifier線程的goal副本更加沒(méi)有影響梭依,GoalNotifier線程自然就感知不到goal變成true了稍算。

那么典尾,要怎樣才能讓GoalNotifier線程役拴,能夠感知到main函數(shù)修改了goal呢?
很簡(jiǎn)單嘛钾埂,讓main函數(shù)修改了goal之后主動(dòng)同步到主內(nèi)存河闰,并且讓GoalNotifier線程在讀取goal的之前,主動(dòng)從主內(nèi)存去取goal褥紫,事實(shí)上姜性,這就是volatile的原理。

volatile的內(nèi)幕

那么volatile是如何讓修改的變量立刻同步到主內(nèi)存的呢髓考?
同樣部念,單看代碼是看不出來(lái)的,volatile只是我們告訴JVM的一個(gè)標(biāo)志氨菇,那么JVM對(duì)于有volatile和沒(méi)有volatile的代碼儡炼,在翻譯成機(jī)器指令時(shí),會(huì)有什么不同呢查蓉?

有同學(xué)會(huì)建議用javap命令反匯編查看一下乌询,如果你也這么想,那現(xiàn)在我直接告訴你豌研,不可以妹田,至于為什么唬党,你可以先自行研究,我將在后面單獨(dú)用一篇文章討論鬼佣。
在這里我們要使用JIT級(jí)別反匯編命令驶拱,原因同樣不在這里贅述。下面簡(jiǎn)單介紹一下方法晶衷。
加入如下虛擬機(jī)參數(shù):

-XX:+UnlockDiagnosticVMOptions -Xcomp -XX:+PrintAssembly -XX:CompileCommand=compileonly,*GoalNotifier.setGoal

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly:開(kāi)啟JIT反匯編
-Xcomp:讓虛擬機(jī)以編譯模式執(zhí)行代碼屯烦,使得JIT編譯可以立即觸發(fā)
-XX:CompileCommand=compileonly,*GoalNotifier.setGoal:只反匯編GoalNotifier的setGoal方法

然后執(zhí)行兩次代碼,一次加入volatile修飾符房铭,一次不加驻龟,把兩次控制臺(tái)打印的匯編語(yǔ)言,放到文件對(duì)比工具上對(duì)比一下缸匪,打印的信息很多翁狐,但是通過(guò)文件對(duì)比工具,我們可以很清楚的看到凌蔬,加了volatile的代碼中露懒,多了一行代碼:



這行“l(fā)ock add dword ptr”的代碼是干什么用的呢?關(guān)鍵在于lock砂心,這個(gè)lock不是指令懈词,而是指令前綴,我對(duì)匯編語(yǔ)言不熟悉辩诞,這里借用《深入學(xué)習(xí)Java虛擬機(jī)》里的解釋?zhuān)骸發(fā)ock的作用是使得本CPU的Cache寫(xiě)入內(nèi)存坎弯,同時(shí)使其他CPU的Cache無(wú)效”,其實(shí)也就是我們上面講的译暂,將修改后的變量主動(dòng)同步到主內(nèi)存抠忘。

加了虛擬機(jī)參數(shù)后,運(yùn)行的時(shí)候你可能會(huì)看到錯(cuò)誤提示外永,別慌崎脉,很容易解決。另外伯顶,我把我做實(shí)驗(yàn)生成的兩份匯編語(yǔ)言以及其他代碼上傳到Github了囚灼,有興趣的同學(xué)可以下載下來(lái)研究。

總結(jié)

對(duì)于volatile這個(gè)關(guān)鍵字祭衩,可能大家都聽(tīng)過(guò)很多遍灶体,但是由于實(shí)際中很少用到,所以大多不太了解其背后的原理汪厨。這次通過(guò)對(duì)volatile的介紹赃春,順帶講解了Java內(nèi)存模型,同時(shí)也看到了Java虛擬機(jī)在Java中的扮演的地位劫乱,還是那句話织中,看Java代碼锥涕,不能只看表象,還要透過(guò)Java虛擬機(jī)狭吼,去看透本質(zhì)层坠。從JavaSE到JVM,這是一場(chǎng)認(rèn)知的躍遷刁笙。

這篇文章與其說(shuō)是講volatile破花,不如說(shuō)是講JVM。對(duì)volatile的介紹也只提到了它在可見(jiàn)性上的作用疲吸,volatile的另一個(gè)作用——禁止指令重排座每,并沒(méi)有提及,畢竟指令重排是個(gè)很高深的家伙摘悴,我也將在后面的文章中和大家一起探討峭梳。

祝大家春節(jié)快樂(lè)!

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蹂喻,一起剝皮案震驚了整個(gè)濱河市葱椭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌口四,老刑警劉巖孵运,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蔓彩,居然都是意外死亡治笨,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)粪小,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)大磺,“玉大人,你說(shuō)我怎么就攤上這事探膊。” “怎么了待榔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵逞壁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我锐锣,道長(zhǎng)腌闯,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任雕憔,我火速辦了婚禮姿骏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斤彼。我一直安慰自己分瘦,他們只是感情好蘸泻,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著嘲玫,像睡著了一般悦施。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上去团,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天抡诞,我揣著相機(jī)與錄音,去河邊找鬼土陪。 笑死昼汗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鬼雀。 我是一名探鬼主播乔遮,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼取刃!你這毒婦竟也來(lái)了蹋肮?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤璧疗,失蹤者是張志新(化名)和其女友劉穎坯辩,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體崩侠,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡漆魔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了却音。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片改抡。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖系瓢,靈堂內(nèi)的尸體忽然破棺而出阿纤,到底是詐尸還是另有隱情,我是刑警寧澤夷陋,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布欠拾,位于F島的核電站,受9級(jí)特大地震影響骗绕,放射性物質(zhì)發(fā)生泄漏藐窄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一酬土、第九天 我趴在偏房一處隱蔽的房頂上張望荆忍。 院中可真熱鬧,春花似錦、人聲如沸刹枉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嘶卧。三九已至尔觉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間芥吟,已是汗流浹背侦铜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钟鸵,地道東北人钉稍。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像棺耍,于是被迫代替她去往敵國(guó)和親贡未。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容