深刻剖析volatile關鍵字

volatile的原理和實現機制

若是一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:html

  1. 保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。
  2. 禁止進行指令重排序。

在Java中long賦值不是原子操做,由於先寫32位,再寫後32位,分兩步操做,而AtomicLong賦值是原子操做,爲何?爲何volatile能替代簡單的鎖,卻不能保證原子性?這裏面涉及volatile,是java中的一個我以爲這個詞在Java規範中從未被解釋清楚的神奇關鍵詞,在Sun的JDK官方文檔是這樣形容volatile的:java

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.c++

意思就是說,若是一個變量加了volatile關鍵字,就會告訴編譯器和JVM的內存模型:這個變量是對全部線程共享的、可見的,每次jvm都會讀取最新寫入的值並使其最新值在全部CPU可見。volatile彷佛是有時候能夠代替簡單的鎖,彷佛加了volatile關鍵字就省掉了鎖。但又說volatile不能保證原子性(java程序員很熟悉這句話:volatile僅僅用來保證該變量對全部線程的可見性,但不保證原子性)。這不是互相矛盾嗎?程序員

volatile到底如何保證可見性和禁止指令重排序的。「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」(摘自《深刻理解Java虛擬機》)lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:緩存

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

例如你讓一個volatile的integer自增(i++);安全

mov    0xc(%r10),%r8d ; Load(讀取volatile變量值到local)
inc    %r8d           ; Increment(增長變量的值)
mov    %r8d,0xc(%r10) ; Store(把local的值寫回,,讓其它的線程可見)
lock addl $0x0,(%rsp) ; StoreLoad Barrier

什麼是內存屏障(Memory Barrier)?

內存屏障(memory barrier)是一個CPU指令。基本上,它是這樣一條指令: a) 確保一些特定操做執行的順序; b) 影響一些數據的可見性(多是某些指令執行後的結果)。編譯器和CPU能夠在保證輸出結果同樣的狀況下對指令重排序,使性能獲得優化。插入一個內存屏障,至關於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。內存屏障另外一個做用是強制更新一次不一樣CPU的緩存。例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將獲得最新值,而不用考慮究竟是被哪一個cpu核心或者哪顆CPU執行的。jvm

內存屏障(memory barrier)和volatile什麼關係?上面的虛擬機指令裏面有提到,若是你的字段是volatile,Java內存模型將在寫操做後插入一個寫屏障指令,在讀操做前插入一個讀屏障指令。這意味着若是你對一個volatile字段進行寫操做,你必須知道:一、一旦你完成寫入,任何訪問這個字段的線程將會獲得最新的值。二、在你寫入前,會保證全部以前發生的事已經發生,而且任何更新過的數據值也是可見的,由於內存屏障會把以前的寫入值都刷新到緩存。ide

volatile爲何沒有原子性?性能

明白了內存屏障(memory barrier)這個CPU指令,回到前面的JVM指令:從Load到store到內存屏障,一共4步,其中最後一步jvm讓這個最新的變量的值在全部線程可見,也就是最後一步讓全部的CPU內核都得到了最新的值,但中間的幾步(從Load到Store)是不安全的,中間若是其餘的CPU修改了值將會丟失。下面的測試代碼能夠實際測試voaltile的自增沒有原子性:測試

volatile可否保證可見性?(能夠)

假如線程1先執行,線程2後執行:

//線程1
boolean stop = false;
while(!stop){
    doSomething();
}
//線程2
stop = true;

  這段代碼是很典型的一段代碼,不少人在中斷線程時可能都會採用這種標記辦法。可是事實上,這段代碼會必定會將線程中斷麼?不必定,也許在大多數時候,這個代碼可以把線程中斷,可是也有可能會致使沒法中斷線程(雖然這個可能性很小,可是隻要一旦發生這種狀況就會形成死循環了)。這是由於每一個線程在運行過程當中都有本身的工做內存,那麼線程1在運行的時候,會將stop變量的值拷貝一份放在本身的工做內存當中。那麼當線程2更改了stop變量的值以後,可是還沒來得及寫入主存當中,線程2轉去作其餘事情了,那麼線程1因爲不知道線程2對stop變量的更改,所以還會一直循環下去。

  若是使用volatile修飾,第一,會強制將修改的值當即寫入主存;第二,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);第三,因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取。那麼在線程2修改stop值時(固然這裏包括2個操做,修改線程2工做內存中的值,而後將修改後的值寫入內存),會使得線程1的工做內存中緩存變量stop的緩存行無效,而後線程1讀取時,發現本身的緩存行無效,它會等待緩存行對應的主存地址被更新以後,而後去對應的主存讀取最新的值。

volatile可否保證原子性?(不能夠)

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的可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性。在前面已經提到過,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:

  假如某個時刻變量inc的值爲10,線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10(由於線程1並無再次讀取Iinc的值,而可見性只能保證每次讀取的是最新的值),因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。那麼兩個線程分別進行了一次自增操做後,inc只增長了1。

把上面的代碼改爲如下任何一種均可以達到效果:

採用synchronized:

public class Test {
    public  int inc = 0;
    public synchronized 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);
    }
}

採用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }
    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);
    }
}

採用AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();
    public  void increase() {
        inc.getAndIncrement();
    }
    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);
    }
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做類,即對基本數據類型的 自增(加1操做),自減(減1操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是原子性操做。atomic是利用CAS來實現原子性操做的(Compare And Swap),CAS其實是利用處理器提供的CMPXCHG指令實現的,而處理器執行CMPXCHG指令是一個原子性操做。

volatile可否保證有序性?(能夠)

在前面提到volatile關鍵字能禁止指令重排序,因此volatile能在必定程度上保證有序性。volatile關鍵字禁止指令重排序有兩層意思:

1)當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;

2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

可能上面說的比較繞,舉個簡單的例子:

//x、y爲非volatile變量
//flag爲volatile變量

x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;        //語句4
y = -1;       //語句5

因爲flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句一、語句2前面,也不會講語句3放到語句四、語句5後面。可是要注意語句1和語句2的順序、語句4和語句5的順序是不做任何保證的。而且volatile關鍵字能保證,執行到語句3時,語句1和語句2一定是執行完畢了的,且語句1和語句2的執行結果對語句三、語句四、語句5是可見的。

那麼咱們回到前面舉的一個例子:

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

前面舉這個例子的時候,提到有可能語句2會在語句1以前執行,那麼久可能致使context還沒被初始化,而線程2中就使用未初始化的context去進行操做,致使程序出錯。這裏若是用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,由於當執行到語句2時,一定能保證context已經初始化完畢。

使用volatile關鍵字的場景

volatile關鍵字在某些狀況下性能要優於synchronized,可是要volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。一般來講,不要將volatile用在getAndOperate場合(這種場不是原子,須要再加鎖),僅僅set或者get的場景是適合volatile的。

下面列舉幾個Java中使用volatile的幾個場景。

1.狀態標記量

volatile boolean flag = false;
while(!flag){
    doSomething();
}
public void setFlag() {
    flag = true;
}
volatile boolean inited = false;
//線程1:
context = loadContext();
inited = true;
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

2.double check

class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

參考地址:http://www.cnblogs.com/dolphin0520/p/3920373.html

相關文章
相關標籤/搜索