volatile關鍵字淺析

1、內存模型

      程序運行過程當中的臨時數據存放在主內存(物理內存)當中的。而從內存中讀寫的速度跟CPU執行指令的速度比起來要慢的多,若是任什麼時候候對數據的操做都要經過和內存的交互來進行,會大大下降指令的執行速度。所以CPU裏面存在高速緩存。
      在程序運行的過程當中,CPU會將運算須要的數據從主存複製一份到其高速緩存中。CPU在進行計算時,直接從其高速緩存讀寫數據。當運算結束以後,再將高速緩存的數據刷新到主存當中。
      當一個變量在多個CPU都存在緩存時,就有可能存在緩存不一致的問題。解決辦法:
html

1)、經過在總線加LOCK#鎖

      由於CPU和其餘部件進行通信時都是經過總線進行的,若是對總線加LOCK#鎖,阻塞了其餘CPU對其餘部件的訪問(如主存)。從而使只有一個CPU能使用這個變量的內存。
      但在鎖住總線期間,其餘CPU沒法訪問內存,致使效率低下。
設計模式

2)、緩存一致性協議

      Intel的MESI協議:當CPU寫數據時,若是發現操做的數據時共享變量,即在其餘CPU中也存在該變量的副本。則會發送信號通知其餘CPU將該變量的緩存置爲無效狀態。所以其餘CPU須要讀取該變量時,發現自身緩存中該變量的緩存是無效的,則會從主存中從新獲取。
緩存

2、併發中的三個概念

      一、原子性:即一個或多個操做,要麼所有執行完畢,要麼都不執行。
      二、可見性:當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看到修改的值。
      三、有序性:程序執行的順序按照代碼的前後順序執行。(處理器會考慮指令間的數據依賴關係來進行重排序)
       如如下例子,代碼1和代碼2進行重排序對結果並沒有影響,但代碼3必須在代碼1,2以後。便可能出現的排序順序是 1,2,3 或者 2,1,3
bash

int a = 10;
int b = 17;
a = a + b;
複製代碼

3、Java內存模型

      Java虛擬機中定義一種Java內存模型以屏蔽各個硬件平臺和操做系統的內存訪問差別。
      Java內存模型沒有限制執行引擎使用處理器的寄存器或高速緩存來提高執行指令,也沒有限制編譯器對指令進行重排序。即Java內存模型也會出現緩存一致性問題和指令重排序的問題。
      Java內存模型規定全部的變量都是存在主存中(相似物理內存),每一個線程都有本身的工做內存(相似CPU的高速緩存)。線程對變量的全部操做必須在工做內存中進行,而不能直接對主存進行操做。而且每一個線程不能訪問其餘線程的工做內存。
多線程

一、原子性

x = 10 //語句1
y = x  //語句2
x++    //語句3
x = x + 1  //語句4
複製代碼

      只有語句1是原子性操做,其餘三個語句都不是原子性操做。
      語句1直接將10賦值給x,也就是說線程執行這個語句時會直接將數值10寫入到工做內存中。而其餘語句實際上包含2個操做,先去讀取x的值,再將x的值寫入工做內存。
併發

      總結:Java內存模型只保證簡單的讀取和賦值(變量之間相互賦值不是原子操做)纔是原子操做。想要保證大範圍操做的原子性,須要經過synchronized和Lock實現,確保任意時刻只有一個線程執行該代碼塊。
高併發

二、可見性

      Java提供volatile關鍵字保證可見性。
      當一個共享變量被volatile修飾時,它會保證修改的值會當即被更新到主存中,當有其餘線程須要讀取時,它會去主存讀取新值。
      而普通的共享變量不能保證可見性,由於普通共享變量被修改後,何時寫入主存是不肯定的。當其餘線程去讀取,此時內存可能仍是原來的舊值。所以沒法保證可見性。
      經過synchronized和Lock也可以保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖,而後執行同步代碼,而且釋放鎖以前會將變量的修改刷新到主存當中,所以能夠確保可見性。
性能

三、有序性

      在Java內存模型中,容許編譯器對指令進行重排序,可是重排序不會影響到單線程的執行,卻影響到多線程併發執行的正確性。
ui

好比new對象時,會進行三件事件:
      (1)、給實例分配內存;
      (2)、調用構造方法,初始化成員變量。
      (3)、將對象指向分配的內存空間。
而在JVM中(2)和(3)的順序是沒法被保證的,只能經過volalite保證其有序性。spa

4、深刻剖析volatile關鍵字

1)保證不一樣線程對變量進行操做時的可見性(即一個線程修改某個變量的值,這新值對其餘線程來講是當即可見的)

2)禁止進行指令重排序。

volatile的原理和實現機制:
      「觀察加入volalite關鍵字和沒有加入volalite關鍵字鎖生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」
      lock前準指令實際上至關於一個內存屏障,內存屏障會提供3個功能:
      一、確保指令重排序時,不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面。
      二、強制對緩存的修改操做當即寫入內存。
      三、若是是寫操做,會致使其餘CPU中對應的緩存無效。

5、使用volatile關鍵字的場景

      synchronized關鍵字是防止多個線程同時執行一段代碼,但會影響執行效率。而volatile關鍵字在某些狀況下性能優於synchronized,但不能替代synchronized,由於volatile不能提供原子性。
      1)對變量的寫操做不依賴於當前值
      2)該變量沒有包含在具備其餘變量的不變式中

我的總結:

      配合計算機的內存模型,能夠很好的理解Java的內存模型。以及瞭解到,可見性在高併發時的重要性。

  • 在確保某個變量(好比某個flag值)在單線程進行修改操做時,可使用volatile確保該變量的可見性。相比synchronized效率有所提高。

  • 在單例DCL中,synchronized時確保了變量的原子性、可見性。但並無確保有序性,這時就須要將變量修飾成volatile來確保有序性

public class Singleton{
    private volatile static Singleton mInstance;
    private Singleton() {}
    
    public static Singleton getInstance(){
        if( mInstance == null){// 語句A
            synchronized(Singleton.class){ 
                if( mInstance == null){ // 語句B
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }
}
複製代碼

由於當指令進行重排序後會出現如下狀況:
(1)、給Singleton的實例分配內存;
(3)、將mInstance對象指向分配的內存空間。
(2)、調用Singleton()的構造方法,初始化成員變量。
      當多線程併發時,線程A先運行到語句A中,mInstance是null,線程A進行單例對象的初始化。但由於指令重排序,出現了(1)(3)(2)的狀況,當線程A還沒執行完(2),也就是還沒初始化完單例對象時。線程B運行到語句A。此時單例對象已不爲null,天然語句A爲false,線程B會返回一個還沒初始化完畢的mInstance對象。

參考文章:

一、www.cnblogs.com/dolphin0520… 二、《Android源碼設計模式》——《單例設計模式》

相關文章
相關標籤/搜索