再有人问你volatile是什么,把这篇文章也发给他。
在上一篇文章中,我們圍繞volatile關(guān)鍵字做了很多闡述,主要介紹了volatile的用法、原理以及特性。在上一篇文章中,我提到過:volatile只能保證可見性和有序性,無法保證原子性。關(guān)于這部分內(nèi)容,有讀者閱讀之后表示還是不是很理解,所以我再單獨(dú)寫一篇文章深入分析一下。
volatile與有序性
在上一篇文章中我們提到過:volatile一個(gè)強(qiáng)大的功能,那就是他可以禁止指令重排優(yōu)化。通過禁止指令重排優(yōu)化,就可以保證代碼程序會(huì)嚴(yán)格按照代碼的先后順序執(zhí)行。那么volatile又是如何禁止指令重排的呢?
先給出結(jié)論:volatile是通過內(nèi)存屏障來來禁止指令重排的。
內(nèi)存屏障(Memory Barrier) 是一類同步屏障指令,是CPU或編譯器在對內(nèi)存隨機(jī)訪問的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點(diǎn)之后的操作。下表描述了和volatile有關(guān)的指令重排禁止行為:
從上表我們可以看出:
當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀時(shí),不能重排序。
具體實(shí)現(xiàn)方式是在編譯期生成字節(jié)碼時(shí),會(huì)在指令序列中增加內(nèi)存屏障來保證,下面是基于保守策略的JMM內(nèi)存屏障插入策略:
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
- 對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
- 對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
- 對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
所以,volatile通過在volatile變量的操作前后插入內(nèi)存屏障的方式,來禁止指令重排,進(jìn)而保證多線程情況下對共享變量的有序性。
volatile與可見性
在上一篇文章中我們提到過:Java中的volatile關(guān)鍵字提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。
其實(shí),volatile對于可見性的實(shí)現(xiàn),內(nèi)存屏障也起著至關(guān)重要的作用。因?yàn)閮?nèi)存屏障相當(dāng)于一個(gè)數(shù)據(jù)同步點(diǎn),他要保證在這個(gè)同步點(diǎn)之后的讀寫操作必須在這個(gè)點(diǎn)之前的讀寫操作都執(zhí)行完之后才可以執(zhí)行。并且在遇到內(nèi)存屏障的時(shí)候,緩存數(shù)據(jù)會(huì)和主存進(jìn)行同步,或者把緩存數(shù)據(jù)寫入主存、或者從主存把數(shù)據(jù)讀取到緩存。
我們在內(nèi)存模型是怎么解決緩存一致性問題的?一文中介紹過緩存緩存一致性協(xié)議,同時(shí)也提到過內(nèi)存一致性模型的實(shí)現(xiàn)可以通過緩存一致性協(xié)議來實(shí)現(xiàn)。同時(shí),留了一個(gè)問題:已經(jīng)有了緩存一致性協(xié)議,為什么還需要volatile?
這個(gè)問題的答案可以從多個(gè)方面來回答:
1、并不是所有的硬件架構(gòu)都提供了相同的一致性保證,Java作為一門跨平臺(tái)語言,JVM需要提供一個(gè)統(tǒng)一的語義。
2、操作系統(tǒng)中的緩存和JVM中線程的本地內(nèi)存并不是一回事,通常我們可以認(rèn)為:MESI可以解決緩存層面的可見性問題。使用volatile關(guān)鍵字,可以解決JVM層面的可見性問題。
3、緩存可見性問題的延伸:由于傳統(tǒng)的MESI協(xié)議的執(zhí)行成本比較大。所以CPU通過Store Buffer和Invalidate Queue組件來解決,但是由于這兩個(gè)組件的引入,也導(dǎo)致緩存和主存之間的通信并不是實(shí)時(shí)的。也就是說,緩存一致性模型只能保證緩存變更可以保證其他緩存也跟著改變,但是不能保證立刻、馬上執(zhí)行。
- 其實(shí),在計(jì)算機(jī)內(nèi)存模型中,也是使用內(nèi)存屏障來解決緩存的可見性問題的(再次強(qiáng)調(diào):緩存可見性和并發(fā)編程中的可見性可以互相類比,但是他們并不是一回事兒)。寫內(nèi)存屏障(Store Memory Barrier)可以促使處理器將當(dāng)前store buffer(存儲(chǔ)緩存)的值寫回主存。讀內(nèi)存屏障(Load Memory Barrier)可以促使處理器處理invalidate queue(失效隊(duì)列)。進(jìn)而避免由于Store Buffer和Invalidate Queue的非實(shí)時(shí)性帶來的問題。
所以,內(nèi)存屏障也是保證可見性的重要手段,操作系統(tǒng)通過內(nèi)存屏障保證緩存間的可見性,JVM通過給volatile變量加入內(nèi)存屏障保證線程之間的可見性。
內(nèi)存屏障
再來總結(jié)一下Java中的內(nèi)存屏障:用于控制特定條件下的重排序和內(nèi)存可見性問題。Java編譯器也會(huì)根據(jù)內(nèi)存屏障的規(guī)則禁止重排序。
volatile與原子性
在以前的文章中,我們介紹synchronized的時(shí)候,提到過,為了保證原子性,需要通過字節(jié)碼指令monitorenter和monitorexit,但是volatile和這兩個(gè)指令之間是沒有任何關(guān)系的。volatile是不能保證原子性的。
網(wǎng)上有很多文章,拿i++的例子說明volatile不能保證原子性,然后進(jìn)行各種分析,有的說由于引入內(nèi)存屏障導(dǎo)致無法保證原子性,有的說一段i++代碼,在編譯后字節(jié)碼為:
10: getfield #2 // Field i:I14: iconst_115: iadd16: putfield #2 // Field i:I在不考慮內(nèi)存屏障的情況下,一個(gè)i++指令也包含了四個(gè)步驟。
這些分析,只是說明了i++本身并不是一個(gè)原子操作,即使使用volatile修飾i,也無法保證他是一個(gè)原子操作。并不能解釋為什么volatile為啥不能保證原子性。
要我說,由于CPU按照時(shí)間片來進(jìn)行線程調(diào)度的,只要是包含多個(gè)步驟的操作的執(zhí)行,天然就是無法保證原子性的。因?yàn)檫@種線程執(zhí)行,又不像數(shù)據(jù)庫一樣可以回滾。如果一個(gè)線程要執(zhí)行的步驟有5步,執(zhí)行完3步就失去了CPU了,失去后就可能再也不會(huì)被調(diào)度,這怎么可能保證原子性呢。
為什么synchronized可以保證原子性 ,因?yàn)楸籹ynchronized修飾的代碼片段,在進(jìn)入之前加了鎖,只要他沒執(zhí)行完,其他線程是無法獲得鎖執(zhí)行這段代碼片段的,就可以保證他內(nèi)部的代碼可以全部被執(zhí)行。進(jìn)而保證原子性。
但是synchronized對原子性保證也不絕對,如果真要較真的話,一旦代碼運(yùn)行異常,也沒辦法回滾。所以呢,在并發(fā)編程中,原子性的定義不應(yīng)該和事務(wù)中的原子性一樣。他應(yīng)該定義為:一段代碼,或者一個(gè)變量的操作,在沒有執(zhí)行完之前,不能被其他線程執(zhí)行。
那么,為什么volatile不能保證原子性呢?因?yàn)樗皇擎i,他沒做任何可以保證原子性的處理。當(dāng)然就不能保證原子性了。
總結(jié)
本文在上一篇文章的基礎(chǔ)上,再次介紹了volatile和原子性、有序性以及可見性之間的關(guān)系。有序性和可見性是通過內(nèi)存屏障實(shí)現(xiàn)的。而volatile是無法保證原子性的。
參考資料
深入理解Java內(nèi)存模型(四)——volatile
總結(jié)
以上是生活随笔為你收集整理的再有人问你volatile是什么,把这篇文章也发给他。的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 那些和闰年相关的 Bug
- 下一篇: NYOJ 714 Card Trick