java併發之volatile關鍵字

Java面試中常常會涉及關於volatile的問題。本文梳理下volatile關鍵知識點。java

volatile字意爲「易失性」,在Java中用作修飾對象變量。它不是Java特有,在C,C++,C#等編程語言也存在,只是在其它編程語言中使用有所差別,但整體語義一致。好比使用volatile 能阻止編譯器對變量的讀寫優化。簡單說,若是一個變量被修飾爲volatile,至關於告訴系統說我容易變化,編譯器你不要隨便優化(重排序,緩存)我。git

Happens-before

規範上,Java內存模型遵行happens-beforegithub

volatile變量在多線程中,寫線程和讀線程具備happens-before關係。也就是寫值的線程要在讀取線程以前,而且讀線程能徹底看見寫線程的相關變量。面試

happens-before:若是兩個有兩個動做AB,A發生在B以前,那麼A的順序應該在B前面而且A的操做對B徹底可見。編程

happens-before 具備傳遞性,若是A發生在B以前,而B發生在C以前,那麼A發生在C以前。緩存

如何保證可見性

多線程環境下counter變量的更新過程。線程1先從主存拷貝副本到CPU緩存,而後CPU執行counter=7,修改完後寫入CPU緩存,等待時機同步到主存。在線程1同步主存前,線程2讀到counter值依然爲0。此時已經發生內存一致性錯誤(對於相同的共享數據,多線程讀到視圖不一致)。由於線程2看不見線程1操做結果,也將這個問題稱爲可見性問題安全

public class SharedObject {
    public int counter = 0;
}

由於多了緩存優化致使,致使可見性問題。因此volatile經過消除緩存(描述可能不太準確)來避免。例如當使用volatile修飾變量後,操做該變量讀寫直接與主存交互,跳過緩存層,保證其它讀線程每次獲取的都是最新值。多線程

public volatile int counter = 0;

volatile

volatile 不單隻消除修飾的變量的緩存。事實上與之相關的變量在讀寫時也會消除緩存,如同使用了volatile同樣。app

以下 years,months,days 三個變量中只有days是volatile,可是對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;
    }
}

這是爲何?咱們分析一下。

一個寫線程調用 update,讀線程調用totalDays。單線程中,對於update方法,wa與wb存在happens-before關係, wawb 以前執行並對wb可見。

多線程中rc與wb存在happens-before關係,wbrc以前執行並對rc可見。根據 happens-before傳遞性,wa須要在rc前先執行並對rc可見。

由於wb是volatile變量,因此rc獲取的years,months也是最新值。

happens-before

咱們知道出於性能緣由,JVM和CPU會對程序中的指令進行從新排序。若是update方法裏面wawb順序被重排,那它們的happens-before關係將不在成立。

happens-before

爲了不這個問題,volatile對重排序作了保證 對於發生在volatile變量操做前的其餘變量的操做不能從新排序

由此咱們獲得volatile經過消除緩存防止重排保證線程的可見性。

volatile保證線程安全?

討論線程安全,你們都會說起原子性順序性可見性。volatile側重於保證可見性,也就是當寫的線程更新後,讀線程總能得到最新值。在只有一個線程寫,多個線程讀的場景下,volatile能知足線程安全。可若是多個線程同時寫入volatile變量時,則須要引入同步語義才能保證線程安全。

模擬10個線程同時寫入volatile變量,一個線程讀counter,執行完後正確結果應該是counter=10。

public static class WriterTask implements Runnable {
        private final ShareObject share;
        private final CountDownLatch countDownLatch;
        public WriterTask(ShareObject share, CountDownLatch countDownLatch) {
            this.share = share;
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            countDownLatch.countDown();
            share.increase();
        }
    }
    
    public class ShareObject {
        private volatile int counter;
        public void increase() {
            this.counter++;
        }
    }

執行結果出現counter=5或6 錯誤結果。

錯誤結果

錯誤結果

經過 synchronized,Lock或AtomicInteger 原子變量保證告終果的正確。

正確結果

完整demo https://gist.github.com/onlythinking/ba7ca7aa5faf00a58f4cedae474fa6f6

volatile性能

volatile變量帶來可見性的保證,訪問volatile變量還防止了指令重排序。不過這一切是以犧牲優化(消除緩存,直接操做主存開銷增長)爲代價,因此不該該濫用volatile,僅在確實須要加強變量可見性的時候使用。

總結

本文記錄了volatile變量經過消除緩存,防止指令重排序來保證線程可見性,而且在多線程寫入的變量的場景下,不保證線程安全。

歡迎你們留言交流,一塊兒學習分享!!!

相關文章
相關標籤/搜索