java併發之volatile關鍵字

mpsky發表於2021-09-09

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

volatile字意為“易失性”,在Java中用做修飾物件變數。它不是Java特有,在C,C++,C#等程式語言也存在,只是在其它程式語言中使用有所差異,但總體語義一致。比如使用volatile 能阻止編譯器對變數的讀寫優化。簡單說,如果一個變數被修飾為volatile,相當於告訴系統說我容易變化,編譯器你不要隨便優化(重排序,快取)我。

Happens-before

規範上,Java記憶體模型遵行happens-before

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一樣。

如下 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變數通過消除快取,防止指令重排序來保證執行緒可見性,並且在多執行緒寫入的變數的場景下,不保證執行緒安全。

歡迎大家留言交流,一起學習分享!!!

相關文章