在上一篇文章中,咱們圍繞volatile關鍵字作了不少闡述,主要介紹了volatile的用法、原理以及特性。在上一篇文章中,我提到過:volatile只能保證可見性和有序性,沒法保證原子性。關於這部份內容,有讀者閱讀以後表示仍是不是很理解,因此我再單獨寫一篇文章深刻分析一下。java
在上一篇文章中咱們提到過:volatile一個強大的功能,那就是他能夠禁止指令重排優化。經過禁止指令重排優化,就能夠保證代碼程序會嚴格按照代碼的前後順序執行。那麼volatile又是如何禁止指令重排的呢?數據庫
先給出結論:volatile是經過內存屏障來來禁止指令重排的。編程
**內存屏障(Memory Barrier)**是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操做中的一個同步點,使得此點以前的全部讀寫操做都執行後才能夠開始執行此點以後的操做。下表描述了和volatile有關的指令重排禁止行爲:緩存
當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。多線程
當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。架構
當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。併發
具體實現方式是在編譯期生成字節碼時,會在指令序列中增長內存屏障來保證,下面是基於保守策略的JMM內存屏障插入策略:優化
StoreStore
屏障。
StoreStore
; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。StoreLoad
屏障。
StoreLoad
; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。LoadLoad
屏障。
LoadLoad
; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。LoadStore
屏障。
LoadStore
; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。在上一篇文章中咱們提到過:Java中的volatile
關鍵字提供了一個功能,那就是被其修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。spa
其實,volatile對於可見性的實現,內存屏障也起着相當重要的做用。由於內存屏障至關於一個數據同步點,他要保證在這個同步點以後的讀寫操做必須在這個點以前的讀寫操做都執行完以後才能夠執行。而且在遇到內存屏障的時候,緩存數據會和主存進行同步,或者把緩存數據寫入主存、或者從主存把數據讀取到緩存。操作系統
咱們在內存模型是怎麼解決緩存一致性問題的?一文中介紹過緩存緩存一致性協議,同時也提到過內存一致性模型的實現能夠經過緩存一致性協議來實現。同時,留了一個問題:已經有了緩存一致性協議,爲何還須要volatile?
這個問題的答案能夠從多個方面來回答:
一、並非全部的硬件架構都提供了相同的一致性保證,Java做爲一門跨平臺語言,JVM須要提供一個統一的語義。
二、操做系統中的緩存和JVM中線程的本地內存並非一回事,一般咱們能夠認爲:MESI能夠解決緩存層面的可見性問題。使用volatile關鍵字,能夠解決JVM層面的可見性問題。
三、緩存可見性問題的延伸:因爲傳統的MESI協議的執行成本比較大。因此CPU經過Store Buffer和Invalidate Queue組件來解決,可是因爲這兩個組件的引入,也致使緩存和主存之間的通訊並非實時的。也就是說,緩存一致性模型只能保證緩存變動能夠保證其餘緩存也跟着改變,可是不能保證馬上、立刻執行。
因此,內存屏障也是保證可見性的重要手段,操做系統經過內存屏障保證緩存間的可見性,JVM經過給volatile變量加入內存屏障保證線程之間的可見性。
再來總結一下Java中的內存屏障:用於控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。
在之前的文章中,咱們介紹synchronized
的時候,提到過,爲了保證原子性,須要經過字節碼指令monitorenter
和monitorexit
,可是volatile
和這兩個指令之間是沒有任何關係的。volatile
是不能保證原子性的。
網上有不少文章,拿i++
的例子說明volatile
不能保證原子性,而後進行各類分析,有的說因爲引入內存屏障致使沒法保證原子性,有的說一段i++
代碼,在編譯後字節碼爲:
10: getfield #2 // Field i:I
14: iconst_1
15: iadd
16: putfield #2 // Field i:I
複製代碼
在不考慮內存屏障的狀況下,一個i++
指令也包含了四個步驟。
這些分析,只是說明了i++
自己並非一個原子操做,即便使用volatile
修飾i
,也沒法保證他是一個原子操做。並不能解釋爲何volatile
爲啥不能保證原子性。
要我說,因爲CPU按照時間片來進行線程調度的,只要是包含多個步驟的操做的執行,自然就是沒法保證原子性的。由於這種線程執行,又不像數據庫同樣能夠回滾。若是一個線程要執行的步驟有5步,執行完3步就失去了CPU了,失去後就可能不再會被調度,這怎麼可能保證原子性呢。
爲何synchronized
能夠保證原子性 ,由於被synchronized
修飾的代碼片斷,在進入以前加了鎖,只要他沒執行完,其餘線程是沒法得到鎖執行這段代碼片斷的,就能夠保證他內部的代碼能夠所有被執行。進而保證原子性。
可是synchronized
對原子性保證也不絕對,若是真要較真的話,一旦代碼運行異常,也沒辦法回滾。因此呢,在併發編程中,原子性的定義不該該和事務中的原子性同樣。他應該定義爲:一段代碼,或者一個變量的操做,在沒有執行完以前,不能被其餘線程執行。
那麼,爲何volatile
不能保證原子性呢?由於他不是鎖,他沒作任何能夠保證原子性的處理。固然就不能保證原子性了。
本文在上一篇文章的基礎上,再次介紹了volatile和原子性、有序性以及可見性之間的關係。有序性和可見性是經過內存屏障實現的。而volatile是沒法保證原子性的。