|
Volatile可能是面試?yán)锩姹貑?wèn)的一個(gè)話題吧,對(duì)他的認(rèn)知很多朋友也僅限于會(huì)用階段,今天我們換個(gè)角度去看看。 先來(lái)跟著丙丙來(lái)看一段demo的代碼: 你會(huì)發(fā)現(xiàn),永遠(yuǎn)都不會(huì)輸出有點(diǎn)東西這一段代碼,按道理線程改了flag變量,主線程也能訪問(wèn)到的呀? 為會(huì)出現(xiàn)這個(gè)情況呢?那我們就需要聊一下另外一個(gè)東西了。 JMM(JavaMemoryModel)
那正式聊之前,丙丙先大概科普一下現(xiàn)代計(jì)算機(jī)的內(nèi)存模型吧。 現(xiàn)代計(jì)算機(jī)的內(nèi)存模型其實(shí)早期計(jì)算機(jī)中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計(jì)算機(jī)中, 將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中,這樣處理器就無(wú)須等待緩慢的內(nèi)存讀寫了。 基于高速緩存的存儲(chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也為計(jì)算機(jī)系統(tǒng)帶來(lái)更高的復(fù)雜度,因?yàn)樗肓艘粋€(gè)新的問(wèn)題: 在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(MainMemory)。 然后我們可以聊一下JMM了。 JMM
JMM有以下規(guī)定:所有的共享變量都存儲(chǔ)于主內(nèi)存,這里所說(shuō)的變量指的是實(shí)例變量和類變量,不包含局部變量,因?yàn)榫植孔兞渴蔷€程私有的,因此不存在競(jìng)爭(zhēng)問(wèn)題。 每一個(gè)線程還存在自己的工作內(nèi)存,線程的工作內(nèi)存,保留了被線程使用的變量的工作副本。
不同線程之間也不能直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量的值的傳遞需要通過(guò)主內(nèi)存中轉(zhuǎn)來(lái)完成。 本地內(nèi)存和主內(nèi)存的關(guān)系:正是因?yàn)檫@樣的機(jī)制,才導(dǎo)致了可見(jiàn)性問(wèn)題的存在,那我們就討論下可見(jiàn)性的解決方案。 可見(jiàn)性的解決方案加鎖為啥加鎖可以解決可見(jiàn)性問(wèn)題呢?因?yàn)槟骋粋€(gè)線程進(jìn)入synchronized代碼塊前后,線程會(huì)獲得鎖,清空工作內(nèi)存,從主內(nèi)存拷貝共享變量最新的值到工作內(nèi)存成為副本,執(zhí)行代碼,將修改后的副本的值刷新回主內(nèi)存中,線程釋放鎖。 而獲取不到鎖的線程會(huì)阻塞等待,所以變量的值肯定一直都是最新的。 Volatile修飾共享變量開(kāi)頭的代碼優(yōu)化完之后應(yīng)該是這樣的: Volatile做了啥?每個(gè)線程操作數(shù)據(jù)的時(shí)候會(huì)把數(shù)據(jù)從主內(nèi)存讀取到自己的工作內(nèi)存,如果他操作了數(shù)據(jù)并且寫會(huì)了,他其他已經(jīng)讀取的線程的變量副本就會(huì)失效了,需要都數(shù)據(jù)進(jìn)行操作又要再次去主內(nèi)存中讀取了。 volatile保證不同線程對(duì)共享變量操作的可見(jiàn)性,也就是說(shuō)一個(gè)線程修改了volatile修飾的變量,當(dāng)修改寫回主內(nèi)存時(shí),另外一個(gè)線程立即看到最新的值。 是不是看著加一個(gè)關(guān)鍵字很簡(jiǎn)單,但實(shí)際上他在背后含辛茹苦默默付出了不少,我從計(jì)算機(jī)層面的緩存一致性協(xié)議解釋一下這些名詞的意義。 之前我們說(shuō)過(guò)當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致,舉例說(shuō)明變量在多個(gè)CPU之間的共享。 如果真的發(fā)生這種情況,那同步回到主內(nèi)存時(shí)以誰(shuí)的緩存數(shù)據(jù)為準(zhǔn)呢? 為了解決一致性的問(wèn)題,需要各個(gè)處理器訪問(wèn)緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)要根據(jù)協(xié)議來(lái)進(jìn)行操作,這類協(xié)議有MSI、 聊一下Intel的MESI吧 MESI(緩存一致性協(xié)議)當(dāng)CPU寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會(huì)發(fā)出信號(hào)通知其他CPU將該變量的緩存行置為無(wú)效狀態(tài),因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí),發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無(wú)效的,那么它就會(huì)從內(nèi)存重新讀取。 至于是怎么發(fā)現(xiàn)數(shù)據(jù)是否失效呢?嗅探每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。 嗅探的缺點(diǎn)不知道大家發(fā)現(xiàn)了沒(méi)有?總線風(fēng)暴由于Volatile的MESI緩存一致性協(xié)議,需要不斷的從主內(nèi)存嗅探和cas不斷循環(huán),無(wú)效交互會(huì)導(dǎo)致總線帶寬達(dá)到峰值。 所以不要大量使用Volatile,至于什么時(shí)候去使用Volatile什么時(shí)候使用鎖,根據(jù)場(chǎng)景區(qū)分。 我們?cè)賮?lái)聊一下 禁止指令重排序什么是重排序?為了提高性能,編譯器和處理器常常會(huì)對(duì)既定的代碼執(zhí)行順序進(jìn)行指令重排序。 重排序的類型有哪些呢?源碼到最終執(zhí)行會(huì)經(jīng)過(guò)哪些重排序呢?一個(gè)好的內(nèi)存模型實(shí)際上會(huì)放松對(duì)處理器和編譯器規(guī)則的束縛,也就是說(shuō)軟件技術(shù)和硬件技術(shù)都為同一個(gè)目標(biāo),而進(jìn)行奮斗:在不改變程序執(zhí)行結(jié)果的前提下,盡可能提高執(zhí)行效率。 JMM對(duì)底層盡量減少約束,使其能夠發(fā)揮自身優(yōu)勢(shì)。 因此,在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序。 一般重排序可以分為如下三種:
這里還得提一個(gè)概念, as-if-serial不管怎么重排序,單線程下的執(zhí)行結(jié)果不能被改變。 編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。 那Volatile是怎么保證不會(huì)被執(zhí)行重排序的呢?內(nèi)存屏障java編譯器會(huì)在生成指令系列時(shí)在適當(dāng)?shù)奈恢脮?huì)插入 為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,JMM會(huì)限制特定類型的編譯器和處理器重排序,JMM會(huì)針對(duì)編譯器制定volatile重排序規(guī)則表: 需要注意的是:volatile寫是在前面和后面分別插入內(nèi)存屏障,而volatile讀操作是在后面插入兩個(gè)內(nèi)存屏障。 寫![]() 讀![]() 上面的我提過(guò)重排序原則,為了提高處理速度,JVM會(huì)對(duì)代碼進(jìn)行編譯優(yōu)化,也就是指令重排序優(yōu)化,并發(fā)編程下指令重排序會(huì)帶來(lái)一些安全隱患:如指令重排序?qū)е碌亩鄠€(gè)線程操作之間的不可見(jiàn)性。 如果讓程序員再去了解這些底層的實(shí)現(xiàn)以及具體規(guī)則,那么程序員的負(fù)擔(dān)就太重了,嚴(yán)重影響了并發(fā)編程的效率。 從JDK5開(kāi)始,提出了 happens-before如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn),那么這兩個(gè)操作之間必須存在happens-before關(guān)系。
如果現(xiàn)在我的變了flag變成了false,那么后面的那個(gè)操作,一定要知道我變了。 聊了這么多,我們要知道Volatile是沒(méi)辦法保證原子性的,一定要保證原子性,可以使用其他方法。 無(wú)法保證原子性就是一次操作,要么完全成功,要么完全失敗。 假設(shè)現(xiàn)在有N個(gè)線程對(duì)同一個(gè)變量進(jìn)行累加也是沒(méi)辦法保證結(jié)果是對(duì)的,因?yàn)樽x寫這個(gè)過(guò)程并不是原子性的。 要解決也簡(jiǎn)單,要么用原子類,比如AtomicInteger,要么加鎖( 應(yīng)用![]() 單例有8種寫法,我說(shuō)一下里面比較特殊的一種,涉及Volatile的。 大家可能好奇為啥要雙重檢查?如果不用Volatile會(huì)怎么樣?我先講一下 對(duì)象實(shí)際上創(chuàng)建對(duì)象要進(jìn)過(guò)如下幾個(gè)步驟:
上面我不是說(shuō)了嘛,是可能發(fā)生指令重排序的,那有可能構(gòu)造函數(shù)在對(duì)象初始化完成前就賦值完成了,在內(nèi)存里面開(kāi)辟了一片存儲(chǔ)區(qū)域后直接返回內(nèi)存的引用,這個(gè)時(shí)候還沒(méi)真正的初始化完對(duì)象。 但是別的線程去判斷instance!=null,直接拿去用了,其實(shí)這個(gè)對(duì)象是個(gè)半成品,那就有空指針異常了。 可見(jiàn)性怎么保證的?因?yàn)榭梢?jiàn)性,線程A在自己的內(nèi)存初始化了對(duì)象,還沒(méi)來(lái)得及寫回主內(nèi)存,B線程也這么做了,那就創(chuàng)建了多個(gè)對(duì)象,不是真正意義上的單例了。 上面提到了volatile與synchronized,那我聊一下他們的區(qū)別。 volatile與synchronized的區(qū)別volatile只能修飾實(shí)例變量和類變量,而synchronized可以修飾方法,以及代碼塊。 volatile保證數(shù)據(jù)的可見(jiàn)性,但是不保證原子性(多線程進(jìn)行寫操作,不保證線程安全);而synchronized是一種排他(互斥)的機(jī)制。 volatile用于禁止指令重排序:可以解決單例雙重檢查對(duì)象初始化代碼執(zhí)行亂序問(wèn)題。 volatile可以看做是輕量版的synchronized,volatile不保證原子性,但是如果是對(duì)一個(gè)共享變量進(jìn)行多個(gè)線程的賦值,而沒(méi)有其他的操作,那么就可以用volatile來(lái)代替synchronized,因?yàn)橘x值本身是有原子性的,而volatile又保證了可見(jiàn)性,所以就可以保證線程安全了。 ![]() 總結(jié)
注:以上所有的內(nèi)容如果能全部掌握我想Volatile在面試官那是很加分了,但是我還沒(méi)講到很多關(guān)于計(jì)算機(jī)內(nèi)存那一塊的底層,那大家就需要后面去補(bǔ)課了,如果等得及,也可以等到我寫計(jì)算機(jī)基礎(chǔ)章節(jié)。 絮叨
因?yàn)楦挛恼潞鸵曨l,丙丙已經(jīng)半年多的周末沒(méi)休息了,都是在公司那個(gè)工位沖沖沖,一直想找時(shí)間出去玩,想著年假一天沒(méi)用,就請(qǐng)了兩天出去玩一下。 這樣五一就可以早點(diǎn)回來(lái),準(zhǔn)備恢復(fù)視頻的更新,你在看的時(shí)候呢,敖丙應(yīng)該在出游的列車上了,是的我就背了這個(gè)包,到寫完的時(shí)候,我還沒(méi)確定去哪里,提前祝大家節(jié)日愉快。 我是敖丙,一個(gè)在互聯(lián)網(wǎng)茍且偷生的工具人。 你知道的越多,你不知道的越多,人才們的 【三連】 就是丙丙創(chuàng)作的最大動(dòng)力,我們下期見(jiàn)! |
|
|