大雄的門(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)算速度的一層高速緩存:
這樣页眯,在上面的++操作里面,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)似:
現(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è)!