Basic Of Concurrency(七: volatile關鍵字)

Java中volatile關鍵字用於標記Java變量「始終存儲在主存中」.這意味着每次都是從主存中讀取volatile修飾的變量,且每次對volatile修飾的變量的更改都會寫回到主存中,而不是cpu緩存.html

事實上,自java5以後,volitle關鍵字保證的不僅是始終在主存中讀取和修改volatile修飾的變量.java

變量可見性問題

volatile保證了線程間對共享變量的修改是可見的.緩存

在多線程應用中,對於沒有volatile修飾的變量,每一個線程在執行過程當中都會從主存中拷貝一份變量的副本到cpu緩存中.假設計算機中擁有多個cpu,每一個線程可能在不一樣的cpu上執行,即每一個線程極可能將變量加載到不一樣的cpu緩存中.如圖所示:多線程

沒有volatile修飾將不能保證JVM什麼時候從主存中讀取變量或什麼時候將變量寫回主存.這將致使若干問題的發生.app

假設兩個或更多的線程訪問一個包含有counter變量的共享對象,聲明以下:post

public class SharedObject{
    public int counter = 0;
}
複製代碼

想象一下,當只有線程1對counter進行累加計算,但線程1和線程2在日後的時間會不定時的從主存中加載變量counter.性能

若是沒有將變量counter修飾爲volatile將不能保證對變量counter的修改會在什麼時候寫回主存.這意味着不一樣cpu緩存中counter變量的值可能與主存中不同.如圖所示:this

問題在於線程1對counter變量的修改對於線程2不可能見.這種一個線程對共享變量的修改對於另外一個線程不可見的問題,咱們稱之爲"可見性"問題.spa

volatile對於可見性的保障

Java中 volatile關鍵字用於解決可見性問題.若將counter修飾爲volatile,那麼全部對於counter的修改會被當即寫回到主存中,且限制counter只能從主存中讀取.線程

public class SharedObject{
    public volatile int counter = 0;
}
複製代碼

將變量修飾爲volatile保證了變量修改對其餘線程的可見性.

上文中說起線程1對counter的修改,線程2對counter的讀取可以經過volatile來保證線程1對counter變量的修改對於線程2可見.

然而,若是線程1和2同時累加counter變量,此時僅僅將變量修飾爲volatile是不夠的.詳情在下文會說起.

volatile對可見性的充分保障

事實上,volatile對於可見性保障不只僅侷限於volatile修飾的變量自己.可見性保障內容以下所示:

  • 若是線程1修改volatile修飾的變量,緊接着線程2讀取一樣volatile修飾的變量,那麼線程1對修改volatile變量以前其餘變量的修改都會對線程2可見.
  • 若是線程1讀取一個volatile修飾的變量,那麼volatile修飾變量以後用到的其餘變量都會強制從主存中讀取以保證全部變量對於線程1可見.

代碼實例:

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}
複製代碼

update()方法用於更新三個變量,其中只有變量days是volatile修飾的.

volatile對可見性的充分保障意味着當線程更新days的值時,會連同days以前的yeas months更新也寫回主存中.

當讀取years months days時:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}
複製代碼

注意totalDays()方法,一開始將days的值賦予total,緊接着連同參與計算的months和years也一塊兒從主存中讀取.所以你能夠保障上面days months和years的讀取都是最新的.

指令重排序帶來的挑戰

JVM和CPU可以在語義相同的狀況下對程序中的指令進行重排序以達到更好的執行效率.以下所示:

int a = 1;
int b = 2;

a++;
b++;
複製代碼

這些指令可以在語義一致的狀況下從新調整順序:

int a = 1;
a++;

int b = 2;
b++;
複製代碼

然而,當重排序中有volatile修飾的變量時,將會帶來一些挑戰.

再來看看以前說起的實例:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}
複製代碼

一旦update()方法更新days變量,那麼對於years和months的更新也會被寫回到主存中,若JVM進行重排序:

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}
複製代碼

當對days變量進行修改時,months和years的修改也會被寫回主存,但此次對days的修改是在months和years以前,所以對於months和years的最新修改不會對其餘線程可見.重排序後的語義已經發生改變.

volatile關於Happens-Before的保障

針對指令重排序的挑戰,volatile給出了"happens-before"保障,用於補充可見性保障.happens-before保障的內容以下所示:

  • 若對於其餘變量的讀寫原順序是在寫volatile修飾變量以前進行的,不能被重排序爲以後進行.保證了寫volatile變量以前對其餘變量的讀寫操做正常的發生.相反,容許對於其餘變量的讀寫原順序是在寫volatile修飾變量以後的,被重排序爲以前進行.
  • 若對於其餘變量的讀寫原順序是在讀volatile變量以後的,不能被重排序爲以前進行.保證了讀volatile變量以後對其餘變量的讀寫操做正常的發生.相反,容許對於其餘變量的讀寫原順序是在讀volatile修飾變量以前的,被重排序爲以後進行.

happens-before保證了volatile可見性保障的強制執行.

volatile並不老是足夠的

儘管volatile保障了volatile修飾的變量老是從主存中讀取和寫回主存,但仍是有些狀況即便將變量修飾爲volatile也不能知足.

以前的狀況是線程1對於volatile變量的修改老是對於線程2可見.

在多線程下,若是產生的新值並不依賴主存中的舊值(不須要使用舊值來推導出新值),那麼即便兩個線程同時更新主存中volatile修飾的變量值也不會有問題.

當一個線程產生的新值須要依賴舊值時,那麼僅僅用volatile修飾共享變量來保障可見性是不夠的.當一個線程在讀取主存中volatile修飾的共享變量以前,此時有兩個線程同時從主存中加載相同的volatile修飾的變量,同時進行更新且寫回主存時會產生競態條件,此時兩個線程對舊值的更新會互相覆蓋.那麼以後線程從主存中讀取的數值多是錯誤的.

這種狀況下,當兩個線程同時累加相同的counter變量時,用volatile修飾變量已經不能知足了.以下所示:

當線程1從主存加載counter到cpu緩存中,此時counter爲0,對counter進行累加以後counter變爲1,此時線程1尚未將counter寫回主存,線程2一樣將主存中counter加載到cpu緩存中進行累加操做.此時線程2也尚未將counter寫回主存.

實際上線程1和線程2是同時進行的.而主存中counter變量的預期結果應該爲2,但如圖所示兩個線程在各自緩存中的值爲1,而在主存中的值爲0.即便兩個線程將各自緩存中的值寫回主存也是錯誤的.

volatile什麼狀況纔是足夠的?

兩個線程同時對一個共享變量進行讀寫操做時,使用volatile修飾已經不能知足狀況.你須要使用synchronized來保障變量讀寫操做的原子性.使用volatile並不能同步線程的讀寫操做.這種狀況下只能使用synchronized關鍵字來修飾臨界區代碼.

除了synchronized,你還能夠選擇java.util.concurrent包中提供的原子數據類型.如AtomicLongAtomicRefrerence等.

在其餘狀況下,若是隻有一個線程對volatile修飾的變量進行讀寫操做,其餘線程只進行讀操做,那麼volatile是足夠保障可見性的,若沒有volatile修飾,那就不能保障了.

volatile關鍵字在32bit和64上的變量可用.

volatile實踐建議

對於volatile修飾變量的讀寫可以被強制在主存中進行(從主存中讀取,寫回主存).直接在主存中讀寫的性能消耗遠大於在cpu緩存中讀寫.volatile可以在特定狀況有效防止指令重排序.因此應該謹慎使用volatile,只有在真正須要保障變量可見性的狀況下使用.

該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 同步代碼塊
下一篇: ThreadLocal

相關文章
相關標籤/搜索