再有人問你volatile是什麼,把這篇文章也發給他。

上一篇文章中,咱們圍繞volatile關鍵字作了不少闡述,主要介紹了volatile的用法、原理以及特性。在上一篇文章中,我提到過:volatile只能保證可見性和有序性,沒法保證原子性。關於這部份內容,有讀者閱讀以後表示仍是不是很理解,因此我再單獨寫一篇文章深刻分析一下。java

volatile與有序性

在上一篇文章中咱們提到過:volatile一個強大的功能,那就是他能夠禁止指令重排優化。經過禁止指令重排優化,就能夠保證代碼程序會嚴格按照代碼的前後順序執行。那麼volatile又是如何禁止指令重排的呢?數據庫

先給出結論:volatile是經過內存屏障來來禁止指令重排的。編程

**內存屏障(Memory Barrier)**是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操做中的一個同步點,使得此點以前的全部讀寫操做都執行後才能夠開始執行此點以後的操做。下表描述了和volatile有關的指令重排禁止行爲:緩存

從上表咱們能夠看出:

當第二個操做是volatile寫時,無論第一個操做是什麼,都不能重排序。這個規則確保volatile寫以前的操做不會被編譯器重排序到volatile寫以後。多線程

當第一個操做是volatile讀時,無論第二個操做是什麼,都不能重排序。這個規則確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。架構

當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序。併發

具體實現方式是在編譯期生成字節碼時,會在指令序列中增長內存屏障來保證,下面是基於保守策略的JMM內存屏障插入策略:優化

  • 在每一個volatile寫操做的前面插入一個StoreStore屏障。
    • 對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。
  • 在每一個volatile寫操做的後面插入一個StoreLoad屏障。
    • 對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。
  • 在每一個volatile讀操做的後面插入一個LoadLoad屏障。
    • 對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
  • 在每一個volatile讀操做的後面插入一個LoadStore屏障。
    • 對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。

因此,volatile經過在volatile變量的操做先後插入內存屏障的方式,來禁止指令重排,進而保證多線程狀況下對共享變量的有序性。

volatile與可見性

在上一篇文章中咱們提到過:Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。spa

其實,volatile對於可見性的實現,內存屏障也起着相當重要的做用。由於內存屏障至關於一個數據同步點,他要保證在這個同步點以後的讀寫操做必須在這個點以前的讀寫操做都執行完以後才能夠執行。而且在遇到內存屏障的時候,緩存數據會和主存進行同步,或者把緩存數據寫入主存、或者從主存把數據讀取到緩存。操作系統

咱們在內存模型是怎麼解決緩存一致性問題的?一文中介紹過緩存緩存一致性協議,同時也提到過內存一致性模型的實現能夠經過緩存一致性協議來實現。同時,留了一個問題:已經有了緩存一致性協議,爲何還須要volatile?

這個問題的答案能夠從多個方面來回答:

一、並非全部的硬件架構都提供了相同的一致性保證,Java做爲一門跨平臺語言,JVM須要提供一個統一的語義。

二、操做系統中的緩存和JVM中線程的本地內存並非一回事,一般咱們能夠認爲:MESI能夠解決緩存層面的可見性問題。使用volatile關鍵字,能夠解決JVM層面的可見性問題。

三、緩存可見性問題的延伸:因爲傳統的MESI協議的執行成本比較大。因此CPU經過Store Buffer和Invalidate Queue組件來解決,可是因爲這兩個組件的引入,也致使緩存和主存之間的通訊並非實時的。也就是說,緩存一致性模型只能保證緩存變動能夠保證其餘緩存也跟着改變,可是不能保證馬上、立刻執行。

  • 其實,在計算機內存模型中,也是使用內存屏障來解決緩存的可見性問題的(再次強調:緩存可見性和併發編程中的可見性能夠互相類比,可是他們並非一回事兒)。寫內存屏障(Store Memory Barrier)能夠促使處理器將當前store buffer(存儲緩存)的值寫回主存。讀內存屏障(Load Memory Barrier)能夠促使處理器處理invalidate queue(失效隊列)。進而避免因爲Store Buffer和Invalidate Queue的非實時性帶來的問題。

因此,內存屏障也是保證可見性的重要手段,操做系統經過內存屏障保證緩存間的可見性,JVM經過給volatile變量加入內存屏障保證線程之間的可見性。

內存屏障

再來總結一下Java中的內存屏障:用於控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。

volatile與原子性

之前的文章中,咱們介紹synchronized的時候,提到過,爲了保證原子性,須要經過字節碼指令monitorentermonitorexit,可是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是沒法保證原子性的。

參考資料

深刻理解Java內存模型(四)——volatile

相關文章
相關標籤/搜索