談談volatile

問題的引入

計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程當中會涉及到數據的讀取和寫入。因爲程序運行過程當中的臨時數據是存放在主內存(物理內存)當中的,這時就存在一個問題,因爲CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,所以若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令執行的速度。爲了處理這個問題,在CPU裏面就有了高速緩存的概念。當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據刷新到主存當中。html

 

圖一(注:L一、L二、L3表示CPU核心中的高速緩存)編程

Java內存模型規定,對於多個線程共享的變量,存儲在主內存當中,每一個線程都有本身獨立的工做內存,線程只能訪問本身的工做內存,不能夠訪問其它線程的工做內存。工做內存中保存了主內存共享變量的副本,線程要操做這些共享變量,只能經過操做工做內存中的副原本實現,操做完畢以後再同步回到主內存當中。線程工做內存一個抽象的描述,工做內存中主要包括兩個部分,一個是屬於該線程私有的棧,二是對主存部分變量拷貝的寄存器(包括程序計數器PC和CPU工做的高速緩存區)。緩存

 

圖二安全

有了以上概念,咱們進一步談談多線程狀況下產生線程不安全的緣由。爲了提升計算機的處理速度,CPU不會直接和內存進行通訊,而是將內存中的數據拷貝到高速緩存中進行操做,當多個線程的共享數據被拷貝到高速緩存後,各個線程對應的那塊高速緩存彼此不可見,而各高速緩存中的數據被CPU修改後不知道什麼時候會被寫入主內存中,這就極有可能致使別的線程讀不到最新數據,從而形成數據不一樣步的線程安全問題。多線程

Java代碼編譯後會變成字節碼在JVM中運行,而字節碼最終須要轉換成彙編語言在CPU上執行,所以Java的併發機制必然依賴於JVM的實現和CPU對指令的執行狀況。併發

volatile的做用及其實現原理

volatile的做用主要有兩點,一是保證變量的可見性,另一個是保證代碼執行的有序性。優化

如有這樣一行代碼:private static volatile LazySingleton instance = new LazySingleton();那麼其轉換成彙編指令的時候大概是這樣:0x0000000002931351:lock add dword ptr [rsp],0h  ;*putstatic instance; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)。加了volatile關鍵字的變量進行寫操做時會多出含有有lock前綴彙編指令,而lock前綴會引起下面的事情:一是當前緩存行中的數據會當即寫入到內存中去,二是這一寫入內存的操做會致使其它高速緩存中緩存了該數據內存地址的數據無效,而且會利用緩存一致性協議來保證其餘處理器中的緩存數據的一致性,從而保證變量的可見性。線程

實際上,當咱們把代碼寫好以後,虛擬機不必定會按照咱們寫的代碼的順序來執行。例如對於這兩句代碼:int a = 1;int b = 2;你會發現不管是先執行a = 1仍是執行b = 2,都不會對a,b最終的值形成影響。因此虛擬機在編譯的時候,是有可能把他們進行重排序的。那麼爲何要進行重排序呢?你想啊,假如執行 int a = 1這句代碼須要100ms的時間,但執行int b = 2這句代碼須要1ms的時間,而且先執行哪句代碼並不會對a,b最終的值形成影響。那固然是先執行int b = 2這句代碼了。因此,虛擬機在進行代碼編譯優化的時候,對於那些改變順序以後不會對最終變量的值形成影響的代碼,是有可能將他們進行重排序的。那麼重排序以後真的不會對代碼形成影響嗎?實際上,對於有些代碼進行重排序以後,雖然對變量的值沒有形成影響,但有可能會出現線程安全問題的。具體請看下面的代碼htm

public class NoVisibility{blog

 

    private static boolean ready;

    private static int number;

 

    private static class Reader extends Thread{

        public void run(){

        while(!ready){

            Thread.yield();

        }

        System.out.println(number);

    }

}

    public static void main(String[] args){

        new Reader().start();

        number = 42;

        ready = true;

    }

}

這段代碼最終打印的必定是42嗎?若是沒有重排序的話,打印的確實會是42,但若是number = 42和ready = true被進行了重排序,顛倒了順序,那麼就有可能打印出0了,而不是42。(由於number的初始值會是0).所以,重排序是有可能致使線程安全問題的。若是一個變量被聲明volatile的話,那麼這個變量不會被進行重排序,也就是說,虛擬機會保證這個變量以前的代碼必定會比它先執行,而以後的代碼必定會比它慢執行。例如把上面中的number聲明爲volatile,那麼number = 42必定會比ready = true先執行。不過這裏須要注意的是,虛擬機只是保證這個變量以前的代碼必定比它先執行,但並無保證這個變量以前的代碼不能夠重排序。以後的也同樣。

緩存一致性協議

全部內存的傳輸都發生在一條共享的總線上,而全部的處理器都能看到這條總線。線程中的處理器會一直在總線上嗅探其內部緩存中的內存地址在其餘處理器的操做狀況(可是注意,當處理器讀取內存中的值後進行寫操做前這段時間即使內存中的值改變了,其高速緩存中的值仍不會失效,能夠認爲這期間線程中的處理器沒有在總線上嗅探其內部緩存中的內存地址在其餘處理器的操做狀況,這爲volatile不能保證線程的安全埋下了伏筆),一旦嗅探到某到處理器打算修改其內存地址中的值,而該內存地址恰好也在本身的內部緩存中,那麼處理器就會強制讓本身對該緩存地址的無效。因此當該處理器要訪問該數據的時候,因爲發現本身緩存的數據無效了,就會去主存中訪問。

爲何說volatile不保證變量在多線程下的安全性?

     內存屏障(lock前綴指令)會把這個屏障前寫入的數據刷新到內存,這樣任何試圖讀取該數據的線程將獲得最新值。但當處理器讀取內存中的值後進行寫操做前這段時間即使內存中的值改變了,其高速緩存中的值仍不會失效,這爲volatile不能保證線程安全埋下了伏筆。這樣若是有一個變量i = 0用volatile修飾,兩個線程對其進行i++操做,若是線程1從內存中讀取i=0進了緩存,而後把數據讀入寄存器,以後時間片用完了,而後線程2也從內存中讀取i進緩存,由於線程1已進行讀操做還未執行寫操做,而後線程2執行完畢,內存中i=1,而後線程1又開始執行,而後將數據寫回緩存再寫回內存,結果仍是1。

參考文獻

  1. 《Java併發編程的藝術》 做者:方騰飛
  2. https://zhuanlan.zhihu.com/p/42497046
  3. https://www.cnblogs.com/xrq730/p/7048693.html#undefined
相關文章
相關標籤/搜索