豁然開朗:這問題我不信你能分析的這么透徹!
- 本章難度:★★☆☆☆
- 本章重點(diǎn):掌握多個(gè)線程同時(shí)讀寫同一共享變量存在共享問題的根本原因,重點(diǎn)掌握CPU內(nèi)存模式和Java內(nèi)存模型的核心原理,緩存一致性問題及其產(chǎn)生的原因,并能夠根據(jù)CPU內(nèi)存模型和Java內(nèi)存模型編寫線程安全的代碼。
大家好,我是冰河~~
“原來如此啊,真沒想到統(tǒng)計(jì)個(gè)調(diào)用商品詳情接口次數(shù)的功能背后還會(huì)牽扯到這么多知識(shí)點(diǎn),這些知識(shí)之前確實(shí)沒聽說過,看來確實(shí)有很多我之前不知道的東西呀,以后跟著老大好好學(xué),爭取跟他一樣厲害”,小菜默默的在心里嘀咕著。
一、情景再現(xiàn)
小菜憑借著之前在學(xué)校的傳奇經(jīng)歷順利進(jìn)入一家頭部互聯(lián)網(wǎng)公司實(shí)習(xí),幾天后,被分配到一個(gè)統(tǒng)計(jì)線上調(diào)用商品詳情接口的任務(wù),本以為很簡單的功能,小菜也三下五除二的完成了開發(fā)任務(wù),但是在測試時(shí),卻被告知小菜統(tǒng)計(jì)出來的結(jié)果和實(shí)際結(jié)果差距太大。經(jīng)過一天的排查和定位,小菜最終也沒有找出問題出在哪里。
二、尋求幫助
第二天,小菜早早來到公司,還在思考著昨天為什么自己寫的代碼明明看起來沒問題,卻跟實(shí)際統(tǒng)計(jì)結(jié)果差距這么大。
正當(dāng)小菜還在糾結(jié)時(shí),他突然聽到:“小菜,怎么樣了,知道昨天為什么會(huì)出現(xiàn)問題了嗎?”。
小菜轉(zhuǎn)過頭一看,原來是自己的直屬領(lǐng)導(dǎo)老王到公司了,“沒有呀,我昨天下班后也在想這個(gè)問題,但是還是沒找到原因”。
此時(shí),小菜起身來到老王的工位旁邊,“老大,昨天我搞到很晚也沒發(fā)現(xiàn)啥問題,你可以給我講講哪里出了問題嗎?”。
“可以”,老王一邊說著,一邊從電腦包里拿出自己的電腦。“其實(shí),要搞清楚為啥昨天你寫的代碼會(huì)出問題,這就涉及到內(nèi)存模型了,說到內(nèi)存模型,就要從CPU內(nèi)存模型和Java內(nèi)存模型兩個(gè)方面進(jìn)行講述了”。
“這樣吧,小菜拿上筆和本,現(xiàn)在會(huì)議室沒人使用,我單獨(dú)給你講講”。
“好的”,小菜邊說,邊回到工位拿筆和本子,老王則拿起了自己的電腦,二人一起到會(huì)議室走去。
三、CPU內(nèi)存模型
“以前了解過CPU內(nèi)存模型嗎?”。
“在學(xué)校聽老師講過,不過講的不夠深入和具體,我也了解的不多,我確實(shí)也想不出來這跟昨天實(shí)現(xiàn)的功能有啥關(guān)系”。
“沒關(guān)系,我今天給你講一下”,老王邊說,邊打開了電腦和投影儀。
“你昨天寫的代碼出問題,本質(zhì)上與內(nèi)存模型有關(guān),說到內(nèi)存模型呢,又會(huì)涉及到CPU內(nèi)存模型和Java內(nèi)存模型,我們先來講講CPU內(nèi)存模型吧”,老王將自己的電腦投到投影儀上。
“好的”,小菜邊聽邊在本子上記。
老王接著說:“Java程序一般都是運(yùn)行在JVM上,JVM本身有自己的內(nèi)存模型,Java的內(nèi)存模型其實(shí)與CPU的內(nèi)存模型有很多相似之處。如果是CPU內(nèi)存模型呢,計(jì)算機(jī)執(zhí)行程序時(shí),每條執(zhí)行指令都是在CPU中執(zhí)行的,并且在CPU執(zhí)行指令的過程中就會(huì)涉及到數(shù)據(jù)的讀寫操作,CPU并不會(huì)直接從計(jì)算機(jī)的磁盤上讀數(shù)據(jù),而是從計(jì)算機(jī)的主存,也就是我們常說的內(nèi)存中讀取數(shù)據(jù),并且CPU也會(huì)將處理的結(jié)果數(shù)據(jù)寫回主存”。
老王一邊說,一邊在腦電上畫出了這樣一張圖,如圖2-1所示。
圖片
“你在大學(xué)里應(yīng)該學(xué)到過,其實(shí)CPU的執(zhí)行速度是非常快的,會(huì)比內(nèi)存的讀寫速度快的多”,老王畫完圖說道。
“是的,這個(gè)我了解過”,小菜回應(yīng)道。
“CPU的執(zhí)行速度和內(nèi)存的讀寫速度存在巨大的速度差,這樣就會(huì)存在一個(gè)問題,由于CPU在處理任務(wù)時(shí),需要從內(nèi)存中讀取數(shù)據(jù),內(nèi)存的讀寫速度遠(yuǎn)遠(yuǎn)不及CPU的執(zhí)行速度,這樣就會(huì)導(dǎo)致CPU的執(zhí)行速度大大下降”。
“為了解決這個(gè)問題,CPU的架構(gòu)師們?cè)贑PU內(nèi)部設(shè)計(jì)了一個(gè)高速緩沖區(qū),用來平緩CPU執(zhí)行速度與內(nèi)存讀寫速度之間的差距。在執(zhí)行時(shí),會(huì)將CPU執(zhí)行任務(wù)要讀取的數(shù)據(jù)從內(nèi)存讀取到CPU的告訴緩沖區(qū),然后CPU再從高速緩沖區(qū)讀取數(shù)據(jù)后執(zhí)行任務(wù)。當(dāng)CPU執(zhí)行完任務(wù),也是先將結(jié)果數(shù)據(jù)寫回到高速緩沖區(qū),隨后再將高速緩沖區(qū)的數(shù)據(jù)刷新到內(nèi)存,這樣CPU的執(zhí)行效率就大大提升了,我們?cè)賮懋嬕粡垐D”。
說完,老王又畫了一張圖,如圖2-2所示。
圖片
“你可以記一下這張圖,這里咱們也暫時(shí)不展開講,如果展開講的話,會(huì)涉及到很多的知識(shí)點(diǎn),比如CPU多級(jí)緩存架構(gòu),緩存一致性,偽共享,內(nèi)存屏障等等很多知識(shí)點(diǎn),一時(shí)半會(huì)兒也講不完,如果今天都講了的話,我估計(jì)你可能也消化不完,所以,關(guān)于CPU內(nèi)存模型,今天就講到這里”。
“好的”,小菜邊聽,邊拿本子記下了這張圖。
“如果你對(duì)CPU多級(jí)緩存架構(gòu),緩存一致性,偽共享,內(nèi)存屏障等等這些知識(shí)點(diǎn)感興趣,想進(jìn)一步學(xué)習(xí)的話,我給你推薦一本書,就是冰河寫的《深入理解高并發(fā)編程:核心原理與案例實(shí)戰(zhàn)》這本書,這本書剖析了并發(fā)編程出現(xiàn)各種詭異Bug問題的根源,從本質(zhì)上深度解析了并發(fā)編程的核心原理,并且給出了很實(shí)用的實(shí)戰(zhàn)案例,吃透這本書,對(duì)你學(xué)習(xí)并發(fā)編程幫助很大,這本書在京東和當(dāng)當(dāng)都在售”。老王繼續(xù)說道。
“好的”,小菜也記下了這本書的書名,準(zhǔn)備入手一本。
“那我們?cè)賮碇v講Java內(nèi)存模型”。
四、Java內(nèi)存模型
“說起Java內(nèi)存模型,其實(shí)與CPU內(nèi)存模型有很多相似的地方,只是說Java內(nèi)存模型中屏蔽了不同操作系統(tǒng)和底層硬件之間的訪問差異,能夠在不同的操作系統(tǒng)和底層硬件之間達(dá)到一致的訪問效果”,老王一邊說,一邊畫圖,畫了一張線程、主內(nèi)存、工作內(nèi)存的關(guān)系圖,如圖2-3所示。
圖片
“Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,也就是存儲(chǔ)在計(jì)算機(jī)的物理內(nèi)存中,每個(gè)線程都有自己的工作內(nèi)存,用于存儲(chǔ)線程私有的數(shù)據(jù),線程對(duì)變量的所有操作都需要在工作內(nèi)存中完成。一個(gè)線程不能直接訪問其他線程工作內(nèi)存中的數(shù)據(jù),只能通過主內(nèi)存進(jìn)行數(shù)據(jù)交互。也就是說,線程在執(zhí)行任務(wù)時(shí),會(huì)先將數(shù)據(jù)從主內(nèi)存復(fù)制到自己的工作內(nèi)存,然后執(zhí)行對(duì)應(yīng)的任務(wù),任務(wù)執(zhí)行完畢后,會(huì)將計(jì)算的結(jié)果數(shù)據(jù),從自己的工作內(nèi)存刷新到主內(nèi)存,這就與CPU內(nèi)存模型很相似了”。
老王邊說邊喝了口水,繼續(xù)道:“你先消化下,CPU內(nèi)存模型和Java內(nèi)存模型,接下來,我們?cè)僦v講你昨天寫的代碼為啥會(huì)出問題”。
“好的”,小菜回應(yīng)道,隨后小菜就在本子上迅速的記著。。。
五、緩存一致性問題
“好了,接下來,我們就來分析下你昨天代碼出現(xiàn)的問題原因吧”。
“好的”。
“我們還是先來看看你昨天寫的代碼”,老王一邊說,一邊打開開發(fā)環(huán)境,打開了小菜昨天寫的代碼。
public class WrongCounter {
private int visitCount;
public void accessVisit(){
visitCount++;
}
public int getVisitCount() {
return visitCount;
}
}
“現(xiàn)在我們就結(jié)合這個(gè)有問題的類來講,假設(shè)同一時(shí)刻有兩個(gè)線程調(diào)用了獲取商品詳情數(shù)據(jù)的接口,兩個(gè)線程都觸發(fā)了WrongCounter類中的accessVisit()方法。也就是說,兩個(gè)線程都執(zhí)行了visitCount++操作,你知道visitCount++操作在內(nèi)存中是如何執(zhí)行的嗎?”,老王問小菜。
“這個(gè)不太清楚”。
“好的,那我們就來講一下,其實(shí)visitCount++操作總體上會(huì)在內(nèi)存中分為三個(gè)步驟”。
1.從主內(nèi)存讀取visitCount的值。
2.將visitCount的值進(jìn)行加1操作。
3.將visitCount的值寫回主內(nèi)存。
“我們一步步講,這樣你也好理解些”,老王繼續(xù)說,“我們先來看第1步:從主內(nèi)存讀取visitCount的值。假設(shè)同一時(shí)刻有兩個(gè)線程同時(shí)調(diào)用了獲取商品詳情的接口,并且觸發(fā)了 visitCount++操作,此時(shí)結(jié)合Java內(nèi)存模型看的話,就像這張圖一樣”。
老王是真厲害,隨手又畫了一張圖,如圖2-4所示。
圖片
“在步驟1從主內(nèi)存讀取visitCount的值時(shí),線程1和線程2都會(huì)把主內(nèi)存中的visitCount值讀取到自己的工作內(nèi)存中,此時(shí)線程1和線程2自身工作內(nèi)存中的visitCount值都是0,這點(diǎn)能理解嗎?”。
“能理解”,小菜回應(yīng)道。
“好,我們?cè)賮斫又v第2步:將visitCount的值進(jìn)行加1操作,還是畫一張圖看的清晰些”,老王又畫了一張圖,如圖2-5所示。
圖片
“此時(shí),線程1和線程2都將讀取到自己工作內(nèi)存中的visitCount的值進(jìn)行加1,此時(shí)線程1和線程2各自工作內(nèi)存中的visitCount值都是1,這點(diǎn)能理解嗎?”。
“能理解”。
“好,我們?cè)賮砜吹?步:將visitCount的值寫回主內(nèi)存,還是來一張圖”,老王確實(shí)厲害,隨手又畫了一張圖,如圖2-6所示。
圖片
“線程1和線程2都會(huì)將自己計(jì)算出的結(jié)果數(shù)據(jù)寫到自身的工作內(nèi)存,再刷新回主內(nèi)存,在實(shí)際場景中,線程1和線程2的執(zhí)行結(jié)果刷新回主內(nèi)存的先后順序是隨機(jī)的,可能是線程1的結(jié)果先刷新回主內(nèi)存,也可能是線程2的結(jié)果先刷新回主內(nèi)存。但無論是先刷新線程1的結(jié)果,還是先刷新線程2的結(jié)果,最終主內(nèi)存中的visitCount的值都是1,這與我們期望的結(jié)果不同,我們期望的結(jié)果是2,實(shí)際結(jié)果卻是1,這下明白昨天你寫的代碼為什么出問題了吧?”。
“明白了,明白了”,小菜連忙回應(yīng)道,”真沒想到寫個(gè)功能還牽扯到這么多知識(shí)點(diǎn),真是又學(xué)到不少知識(shí)呀“。
六、如何解決問題
“現(xiàn)在明白了昨天寫的代碼為何會(huì)出現(xiàn)問題,那知道怎么解決嗎?”,老王問。
“大概知道點(diǎn),但是不是很明白”。
“好,那我們?cè)僦v講怎么解決問題吧”。
“好的”。
“我們?cè)俅慰纯磛isitCount++操作在內(nèi)存中的執(zhí)行流程”。
1.從主內(nèi)存讀取visitCount的值。
2.將visitCount的值進(jìn)行加1操作。
3.將visitCount的值寫回主內(nèi)存。
“既然visitCount++操作在內(nèi)存中的執(zhí)行流程會(huì)被分成3個(gè)步驟,那如果能夠保證這3個(gè)步驟的原子性,也就是說,線程1完全執(zhí)行完畢這三個(gè)步驟,線程2再從主內(nèi)存中讀取數(shù)據(jù)進(jìn)行處理。或者線程2完全執(zhí)行完畢這三個(gè)步驟,線程1再從主內(nèi)存中讀取數(shù)據(jù)進(jìn)行處理,這樣就能保證最終的結(jié)果數(shù)據(jù)與預(yù)期相符。這么說明白了嗎?”。
“明白了”。
“好的,那我們今天就講到這,根據(jù)今天我們講的內(nèi)容,你把昨天寫的代碼嘗試修改下,不明白的地方再來問我”。
“好的,謝謝你,老大,今天確實(shí)學(xué)到不少知識(shí),回去我也好好總結(jié)下”。
七、本章總結(jié)
本章,以故事場景的方式結(jié)合代碼問題,以圖文的形式重點(diǎn)介紹了CPU內(nèi)存模型和Java內(nèi)存模型,CPU內(nèi)存模型和Java內(nèi)存模型雖然平滑了CPU執(zhí)行計(jì)算與讀寫主內(nèi)存之間的速度差異,但是也引入了新的問題,那就是緩存一致性問題。本章,也結(jié)合代碼示例與圖文詳細(xì)介紹了緩存一致性問題。最后,簡單敘述了如何解決相關(guān)的問題。
最后,可以在評(píng)論區(qū)寫下你學(xué)完本章節(jié)的收獲,祝大家都能學(xué)有所成,我們一起搞定高并發(fā)設(shè)計(jì)模式。