volatile的做用及正確的使用模式

volatile

先從基礎的知識提及吧,這樣也有個前因後果。html

咱們都知道,程序運行後,程序的數據都會被從磁盤加載到內存裏面(主存)
clipboard.pngjava

而當局部的指令被執行的時候,內存中的數據會被加載到更加靠近CPU的各級緩存,以及寄存器中。小程序

clipboard.png

當一個多線程程序執行在一個多核心的機器上時,就會出現真正的並行狀況,每一個線程都獨立的運行在一個CPU上,每一個CPU都有屬於本身獨立的周邊緩存。緩存

那麼此時,一個變量被兩個線程操做,從內存複製到CPU緩存時,就可能出現兩份了,這個時候就會出現問題,好比說簡單的自增操做,就會變成,你加你的,我加個人,這樣運行的效果就會偏離預期。線程1在CPU1上只看得見本身的緩存變量,線程2在CPU2上,也只看得見本身的緩存變量,它們都認爲這是正確的、惟一的變量。這樣也就致使程序運行結果偏離了預期。多線程

volatile關鍵字就是用來處理這種可見性的問題。ide

當一個變量被標記爲volatile的時候,這個變量將被放在主存裏面,而不是CPU的緩存裏面。當這個變量被讀取的時候,是從主存讀取,當這個變量被寫入的時候,是寫入到主存。wordpress

總所周知,內存確定是比CPU緩存的讀寫速度要慢的。性能

那,是否是就意味着讀寫volatile的變量,效率會比非volatile的變量低呢測試

爲了驗證這個問題,我寫了一個簡單的程序,分別有3個變量,分別是成員變量,volatile修飾的成員變量,和static靜態變量。google

對它們進行1000000000次自增,而後記錄下完成時間。

以上的操做執行10次,取平均值。

最終獲得瞭如下的結果:

avg--------------
normal: 32.4
volatile: 5828.3
static: 43.8

看來讀寫volatile變量,確實要比普通的變量要慢,但也是數量級很是大的時候,纔會很是明顯。

下面是我作的實驗,右邊比左邊逐漸少一個量級,相對於數量級增加,volatile關鍵字修飾的變量讀寫耗時有了等比的線性增加。

而普通的成員變量和靜態變量就沒有這樣的現象了,可見CPU緩存對性能的巨大提高。

clipboard.png

那麼,使用volatile關鍵字可讓變量老是讀寫於主存,是否是就能夠用它來避免多線程讀寫同一個變量致使的競爭問題呢?

答案是不能

由於,volatile關鍵字只是保證變量的可見性,而沒有保證操做的原子性,所以,只使用這個關鍵字沒法保證操做的原子性。

爲了驗證一下這個問題,我也寫了一個小程序來測試。

有兩個變量a1,a2,a1是普通的成員變量,a2是volatile的成員變量。使用兩個線程對它們進行自增n次,觀察最後的結果是否爲2n。

public int a1 = 0;
public volatile int a2 = 0;

/**
 * 測試volatile是否具有原子性
 * */
public static void test2() {

    TestVolatile tv = new TestVolatile();

    int count = 8000;

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < count; i++) {
                tv.a1++;
            }
            for (int i = 0; i < count; i++) {
                tv.a2++;
            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < count; i++) {
                tv.a1++;
            }
            for (int i = 0; i < count; i++) {
                tv.a2++;
            }
        }
    });

    t1.start();
    t2.start();

    try {
        t1.join();
        t2.join();
    } catch (Throwable e) {

    }

    System.out.println(tv.a1);
    System.out.println(tv.a2);
}

最後的結果是,a1和a2的表現差很少,有時候其中一個會恰好達到2n,但大多數狀況,都是兩個變量都沒法成功的自增到2n(16000)

11135
9527

這也就驗證了volatile關鍵字原子性操做的問題。

那麼在實踐中,若是出現須要同步的問題,依然仍是要使用synchronized來解決,那麼volatile應該在什麼場景下使用呢?

volatile的應用場景

這個問題,我也是查了很是多的資料,花了幾天的時間才搞清楚一點點。

正確的使用場景,基本符合一個原則:一寫多讀:有一個數據,只由一個線程更新,其餘線程都來讀取。

好比,有一個系統回調,告訴咱們最新的設備的經緯度,而其餘的線程須要去用這個經緯度作一些計算,那麼這個經緯度就一直被第一個線程寫,其餘線程就只負責讀取。此時,經緯度就很適合用volatile修飾,這樣能夠保證其餘線程永遠讀取到的是最新的數值。

再好比,咱們有一個WebSocket的封裝類,裏面包裝了長鏈接的創建和發送數據的方法。那麼可能會有好幾個線程要使用這個封裝類發送本身的請求。要求是當長鏈接斷開了,要調用創建長鏈接的方法,爲了維護長鏈接的狀態標記,就須要這麼一個狀態flag,相似於boolean isConnected這種變量。此時,也很符合一寫多讀的場景,那麼這個變量就能夠用volatile進行修飾。

class WebSocketClient {
    volatile boolean isConnected = false;
    
    public void connect() {
        // ... do connect
        if (success) {
            isConnected = true;
        }
    }
    
    public void disconnect() {
        isConnected = false;
    }
}

總結

volatile的做用是很微妙的,它並不能替代synchronized,所以它沒法提供同步的能力,它只能提供改變可見性的能力。

因爲老是讀寫與主存,它的讀寫性能要低於普通的變量。

正確使用的模式總結下來就是一個線程寫,多個線程讀。

但也不要過於迷信它的功效,大部分狀況下,都徹底不須要使用這個關鍵字的。

參考資料

下面是我查閱的資料,供你們在補充閱讀如下哦

若是你喜歡這篇文章,歡迎點贊評論打賞
更多幹貨內容,歡迎關注個人公衆號:好奇碼農君

clipboard.png

相關文章
相關標籤/搜索