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