java併發編程之volatile

Java語言規範第三版中對volatile的定義以下:Java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保經過排他鎖單獨得到這個變量。java

瞭解volatile關鍵字以前須要先了解下Java內存模型,java內存模型抽象示意圖以下:編程

Java內存模型

java內存模型抽象示意圖
線程A和線程B之間若要通訊的話, 必須經歷下面兩個步驟 (1)線程A和線程A本地內存中更新過的共享變量刷新到主存中去。 (2)線程B到主存中去讀取線程A以前更新過的共享變量。

因而可知執行下面的語句:緩存

int a = 100 線程必須如今本身的工做線程中對變量i所在的緩存進行賦值操做,而後再寫入主存當中,而不是直接將數值100寫入主存中。安全

特性

  1. 可見性 當一個共享變量被volatile修飾時,它會保證修改的值當即被更新到主存,因此對其餘線程是可見的。當其餘線程須要讀取該值時,其餘線程會去主存中讀取新值。相反普通的共享變量不能保證可見性,由於普通共享變量被修改後並不會當即被寫入主存,什麼時候被寫入主存也不肯定。當其餘線程去讀取該值時,此時主存可能仍是原來的舊值,這樣就沒法保證可見性。bash

  2. 有序性 java內存模型中容許編譯器和處理器對指令進行重排序,雖然重排序過程不會影響到單線程執行的正確性,可是會影響到多線程併發執行的正確性。這時能夠經過volatile來保證有序性,除了volatile,也能夠經過synchronized和Lock來保證有序性。synchronized和Lock保證每一個時刻只有一個線程執行同步代碼,這至關於讓線程順序執行同步代碼,從而保證了有序性。若是不考慮原子性操做的話volatile比synchronized和Lock更輕量級,成本更低。多線程

  3. 不保障原子性 volatile關鍵字只能保證共享變量的可見性和有序性。若是volatile修飾併發線程中共享變量, 而該共享變量是非原子操做的話,併發中就會出現問題。好比下面代碼:架構

public class HelloVolatile{
    public volatile int mNumber = 0;
    public static void main(String []args){
        final HelloVolatile hello = new HelloVolatile();
        for(int i =0; i<10; i++){
            new Thread(){
                public void run(){
                    for(int j =0; j<1000; j++){
                        hello.mNumber ++;
                    }
                }
            }.start();
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println("number:"+hello.mNumber);
    }
}
複製代碼

這段代碼預期結果是10000,但是每次執行結果都有可能不同。這是由於自增或自減都是非原子操做。併發

(1) 假如mNumber此時等於100,線程1進行自增操做。編程語言

(2)線程1先讀取了mNumber的值100,而後它被堵塞了。ui

(3)這時候線程2讀取mNumber的值100,而後進行了自增操做,並寫入到主存中, 這時候主存中的值爲101。

(4)這時候線程1繼續執行,由於此前線程1已經讀取到值100,而後進行自增操做101,而後將101寫入到主存中。

能夠看到兩個線程分別對100進行了+1操做,預期主存中的nNumber = 102,實際mNumebr = 101; 這就是由於非原子操做形成的。

使用場景

(1)併發編程中不依賴於程序中任意其狀態的狀態標識。能夠經過關鍵字volatile代替synchronized, 提升程序執行效率,並簡化代碼。

(2)單例模式的雙重檢查模式DCL

public class DclSingleton {
    private volatile static DclSingleton mInstance = null;
    public static DclSingleton getInstance(){
        if(mInstance==null){
            synchronized (DclSingleton.class){
                if(mInstance==null){
                    mInstance = new DclSingleton();
                }
            }
        }
        return mInstance;
    }
}
複製代碼

原理淺析

將volatile修飾的變量轉變成彙編代碼,以下:

... lock addl $0x0,(%rsp)

經過查IA-32架構安全手冊可知,Lock前綴指令在多核處理器會引起兩件事。

1)將當前處理器緩存行的數據寫回到系統內存。

2)這個寫回內存的操做會使在其餘CPU裏緩存了該內存地址的數據無效。


解讀 :

爲了提升,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存後再進行操做,但操做完不知道什麼時候再寫回內存。若是對聲明瞭volatile的變量進行寫操做,JVM會向處理機發送一條Lock前綴指令,將這個變量所在的緩存行的數據寫回到系統內存。

可是寫會內存後,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會出現問題。因此在多處理器下,爲了保證各個處理器緩存是一致的,就會實現緩存一致性協議,以下圖:

每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的數據是否過時了,當處理器發現本身的緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態。當處理器對這個數據進行操做的時候,就會從新從系統內存中把數據讀處處理器緩存中。

相關文章
相關標籤/搜索