淺談volatile關鍵字

上次咱們已經學習了Synchronized關鍵字,有興趣的能夠看看:死磕Synchronized實現原理,今天來研究一下volatile關鍵字,主要有如下知識點:html

  • 併發編程中的三個概念
  • 深刻剖析volatile關鍵字
  • volatile的原理和實現機制
  • 使用volatile關鍵字的場景

併發編程中的三個概念

在併發編程中,咱們一般會遇到如下三個問題:原子性問題,可見性問題,有序性問題c++

原子性

一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。編程

一個很經典的例子就是銀行帳戶轉帳問題:好比從帳戶A向帳戶B轉1000元,那麼必然包括2個操做:從帳戶A減去1000元,往帳戶B加上1000元。試想一下,若是這2個操做不具有原子性,會形成什麼樣的後果。假如從帳戶A減去1000元以後,操做忽然停止。 這樣就會致使帳戶A雖然減去了1000元,可是帳戶B沒有收到這個轉過來的1000元。因此這2個操做必需要具有原子性才能保證不出現一些意外的問題。

可見性

當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。segmentfault

舉個簡單的例子,看下面這段代碼:緩存

//線程1執行的代碼
 int i = 0;
 i = 10;
 
 //線程2執行的代碼
 j = i;

倘若執行線程1的是CPU1,執行線程2的是CPU2。當線程1執行 i = 10這句時,會先把i的初始值加載到CPU1的高速緩存中,而後賦值爲10,那麼在CPU1的高速緩存當中i的值變爲10了,卻沒有當即寫入到內存當中。此時線程2執行 j = i,它會先去內存讀取i的值並加載到CPU2的緩存當中,注意此時內存當中i的值仍是0,那麼就會使得j的值爲0,而不是10。
這就是可見性問題,線程1對變量i修改了以後,線程2沒有當即看到線程1修改的值。多線程

有序性

程序執行的順序按照代碼的前後順序執行。併發

首先解釋一下什麼是指令重排序,通常來講,處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。性能

雖然重排序不會影響單個線程內程序執行的結果,可是多線程呢?下面看一個例子:學習

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2
 
//線程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代碼中,因爲語句1和語句2沒有數據依賴性,所以可能會被重排序。
假如發生了重排序,在線程1執行過程當中先執行語句2,而此時線程2會覺得初始化工做已經完成,
那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並無被初始化,就會致使程序出錯。
  
從上面能夠看出,指令重排序不會影響單個線程的執行,可是會影響到線程併發執行的正確性。
也就是說,要想併發程序正確地執行,必需要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會致使程序運行不正確。優化

深刻剖析volatile關鍵字

性質

  • volatile所修飾的變量是直接存在於主內存中的。
  • JMM中的內存分爲主內存和工做內存,其中主內存是全部線程共享的。而工做內存是每一個線程獨立分配的,各個線程的工做內存之間相互獨立、互不可見。
  • 在線程啓動的時候,JVM爲每一個內存分配了一塊工做內存,不只包含了線程內部定義的局部變量,也包含了線程所須要的共享變量的副本,固然這是爲了提升執行效率,讀副本的比直接讀主內存更快。

volatile特性

內存可見性

即線程A對volatile變量的修改,其餘線程獲取的volatile變量都是最新的。
對於volatile修飾的變量(共享變量)來講,在工做內存發生了變化後,必需要立刻寫到主內存中,而線程讀取到是volatile修飾的變量時,必須去主內存中去獲取最新的值,而不是讀工做內存中主內存的副本,這就有效的保證了線程之間變量的可見性

能夠禁止指令重排序

那麼什麼是指令重排序?
爲了儘量減小內存操做速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照本身的一些規則將程序編寫順序打亂

volatile能保證原子性嗎?

volatile能夠保證線程的可見性和有序性,但沒辦法保證對變量的操做的原子性。

在Java中,對基本數據類型的變量的讀取和賦值操做是原子性操做,即這些操做是不可被中斷的,要麼執行,要麼不執行。

請分析如下哪些操做是原子性操做:

x = 10;        //語句1
  y = x;         //語句2
  x++;           //語句3
  x = x + 1;     //語句4

只有語句1是原子性操做,其餘三個語句都不是原子性操做。

語句1是直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工做內存中。
語句2實際上包含2個操做,它先要去讀取x的值,再將x的值寫入工做內存,雖然讀取x的值以及將x的值寫入工做內存這2個操做都是原子性操做,可是合起來就不是原子性操做了。
一樣的,x++和 x = x+1包括3個操做:讀取x的值,進行加1操做,寫入新的值。

下面看一個例子:

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。上面的程序錯在沒能保證原子性。

volatile的原理和實現機制

下面這段話摘自《深刻理解Java虛擬機》:
觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令。
lock前綴指令實際上至關於一個內存屏障,內存屏障會提供3個功能:

  • 它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;
  • 它會強制將對緩存的修改操做當即寫入主存;
  • 若是是寫操做,它會致使其餘CPU中對應的緩存行無效。

使用volatile關鍵字的場景

synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized,可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。

一般來講,使用volatile必須具有如下2個條件:

  • 對變量的寫操做不依賴於當前值
  • 該變量沒有包含在具備其餘變量的不變式中

事實上,個人理解就是上面的2個條件須要保證操做是原子性操做,才能保證使用volatile關鍵字的程序在併發時可以正確執行。

參考

volatile關鍵字

相關文章
相關標籤/搜索