volatile如何避免指令重排序?原來使用了內存屏障

在多線程的世界裏,一共有三個問題:原子性問題、可見性問題、有序性問題。整個java併發體系也是圍繞着如何解決這三個問題來設計的。volatile關鍵字也不例外,我們都知道它解決了可見性和有序性,但是不能保證原子性。這篇文章也主要基於其中一個特性,也就是研究一下volatile是如何保證有序性的。

一、有序性

1、有序性案例
有序性指的是:程序執行的順序按照代碼的先後順序執行。我們可以先看一個被列舉了一萬次的代碼:

int i = 0;              
boolean flag = false;
i = 1;                //語句1  
flag = true;          //語句2

按照我們自己常規的想法,順序應該從上往下依次執行,但是真實情況是:jvm會在真正執行這段代碼的時候進行優化,發生指令的重排序。因此不能保證語句1一定在語句2先執行。

按照我們自己常規的想法,順序應該從上往下依次執行,但是真實情況是:jvm會在真正執行這段代碼的時候進行優化,發生指令的重排序。因此不能保證語句1一定在語句2先執行。

2、數據依賴性
上面的例子,你還會發現這樣一個特點,就算是發生了指令的重排序,但是最後的結果總是正確的。我們再舉一個例子:

int a = 10;    //語句1
int r = 2;     //語句2
a = a + 3;     //語句3
r = a*a;       //語句4

這種情況會發生指令重排序嗎?顯然不會,原因是處理器在進行重排序時是會考慮指令之間的數據依賴性,如果一個指令2必須用到指令1的結果,那麼處理器一定保證指令1在指令2執行。

3、多線程問題
這種數據的依賴性在單線程環境下一點問題沒有,因爲總能保證數據的正確,但是在多線程環境下就會出現錯誤。我們再舉一個例子:

class Test {
    int a = 0;
    boolean flag = false;
    public void do1() {
        a = 1;                  // 1
        flag = true;            // 2
    }
    public void do2() {
        if (flag) {             // 3
            int i = a * a;      // 4
        }
    }
}

上面的這段代碼由於語句1和語句2沒有數據依賴性,因此會發生指令重排。do2只要看到flag爲true,就執行。因此可能的順序是:

(1)語句1先於語句2:語句2->語句3->語句1->語句4。這時候的結果i=1。

(1)語句2先於語句1:語句2->語句3->語句4->語句1。這時候的結果i=0。

現在我們可以看到在多線程環境下如果發生了指令的重排序,會對結果造成影響。

上面一開始提到過,volatile可以保證有序性,也就是可以防止指令重排序。那麼它是如何解決的呢?這就是內存屏障。因此我們從內存屏障講起。

二、內存屏障

1、什麼是內存屏障
內存屏障其實就是一個CPU指令,在硬件層面上來說可以扥爲兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。主要有兩個作用:

(1)阻止屏障兩側的指令重排序;

(2)強制把寫緩衝區/高速緩存中的髒數據等寫回主內存,讓緩存中相應的數據失效。

在JVM層面上來說作用與上面的一樣,但是種類可以分爲四種:

在這裏插入圖片描述
2、volatile如何保證有序性?
首先一個變量被volatile關鍵字修飾之後有兩個作用:

(1)對於寫操作:對變量更改完之後,要立刻寫回到主存中。

(2)對於讀操作:對變量讀取的時候,要從主存中讀,而不是緩存。

OK,現在針對上面JVM的四種內存屏障,應用到volatile身上。因此volatile也帶有了這種效果。其實上面提到的這些內存屏障應用的效果,可以是用happen-before來總結歸納。

3、內存屏障分類
內存屏障有三種類型和一種僞類型:

(1)lfence:即讀屏障(Load Barrier),在讀指令前插入讀屏障,可以讓高速緩存中的數據失效,重新從主內存加載數據,以保證讀取的是最新的數據。

(2)sfence:即寫屏障(Store Barrier),在寫指令之後插入寫屏障,能讓寫入緩存的最新數據寫回到主內存,以保證寫入的數據立刻對其他線程可見。

(3)mfence,即全能屏障,具備ifence和sfence的能力。

(4)Lock前綴:Lock不是一種內存屏障,但是它能完成類似全能型內存屏障的功能。

爲什麼說Lock是一種僞類型的內存屏障,是因爲內存屏障具有happen-before的效果,而Lock在一定程度上保證了先後執行的順序,因此也叫做僞類型。比如,IO操作的指令,當指令不執行時,就具有了mfence的功能。

OK,一句話說完就是內存屏障保證了volatile的有序性。當然,我在知乎等很多平臺也看到了從計算機底層角度來分析的。還特地去看了看相關文獻。發現這裏面要是詳細寫,不是一兩篇就能完成的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章