1. 程式人生 > 其它 >通俗易懂:volatile怎麼保證可見性?

通俗易懂:volatile怎麼保證可見性?

技術標籤:技術交流面試javavolatile

volatile怎麼保證可見性

前言

你好!如果你看到這裡我預設你具備基本的併發程式設計能力。
隨著硬體的能力擴充套件,軟體效能提升中的硬體的“免費的午餐”似乎暫時的到了一個瓶頸期,所以在效能提升中軟體多執行緒成了主要的麵包。在進入多執行緒程式設計後我們會涉及三個主要的併發概念即:可見性 原子性和有序性。

可見性

什麼是可見性?

所謂的可見性就是指可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

其根本原因是因為每一個執行緒擁有主記憶體的一個副本,其執行過程中是使用副本中的資料,最後會將副本中的資料寫回主存。那麼就存在一個多執行緒對一個變數的操作結果被覆蓋的問題。就是說A執行緒修改一個變數值後,B執行緒還在使用老的值最終導致資料不一致。

其實我們也能擴充套件開想到:執行緒不安全的一個重要原因就是資料不可見性導致。

volatile與可見性

volatile底層的原理還是lock加鎖實現。

1.Lock字首指令會引起處理器快取會寫到記憶體
當對volatile變數進行寫操作的時候,JVM會向處理器傳送一條lock字首的指令,將這個快取中的變量回寫到系統主存中
2.一個處理器的快取回寫到記憶體會導致其他處理器的快取失效

處理器使用嗅探技術保證內部快取 系統記憶體和其他處理器的快取的資料在總線上保持一致。
如果一個變數被volatile所修飾的話,在每次資料變化之後,其值都會被強制刷入主存。而其他處理器的快取由於遵守了快取一致性協議,也會把這個變數的值從主存載入到自己的快取中。這就保證了一個volatile在併發程式設計中,其值在多個快取中是可見的。

有序性

什麼是有序性?

有序性的誕生主要是為了提高效能。由於cpu與記憶體之間的效能差異,虛擬機器會自動為我們調整程式碼執行順序,你可以簡單理解為會把每一次次的記憶體互動指令通過重排序後放在一塊去“批量執行”,把高效的cpu執行也放在一塊去執行。提高總體效能。

volatile與有序性

你要禁止一個新冠患者傳染別人怎麼辦?最簡單粗暴的辦法就是砌一堵牆隔離他們。同樣的簡單粗暴,理解一個名詞 “記憶體屏障”,即volatile實現有序性的辦法。
記憶體屏障有4中分別是:
StoreStore StoreLoad LoadLoad LoadStore
其中Load是讀 Store是寫。我以StoreStore 舉個例子:
寫寫屏障
一旦有了StoreStore 屏障,那麼這操作將會變成序列。其他屏障同理。

原子性

什麼是原子性

在資料庫層面的事務知識中我們經常會說 ACID 其中A就是指的原子性。

原子性我們可以理解為最小的操作單元無法再被分割。
因為再多執行緒的場景下,如果你不是最小執行單元即你這個命令需要多個動作完成,那麼執行緒的cpu許可權完全有可能在你執行到一半的時候被切換。而當你重新回來的時候,已然物是人非(你依賴的引數可能已經被其他執行緒所修改,故已經執行緒不安全了)

所以說事務必須具有原子性也是一個道理,原子性不會被中途切分是最小的執行單元。

volatile與原子性

注意:volatile無法保證原子性
因為volatile它不是鎖,只是一個變數修飾符,所以無法保證原子性。

public class Test {
    public volatile int i = 0;

    public void increase() {
        i++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保證前面的執行緒都執行完
            Thread.yield();
        System.out.println(test.i);
    }
}


上面這段程式碼就是建立10個執行緒,然後分別執行1000次 i++操作。正常情況下,程式的輸出結果應該是10000,但是,多次執行的結果都小於10000。所以說volatile無法滿足原子性。

i++操作在編譯後位元組碼如下:

getfield      #2                  // Field i:I
iconst_1
iadd
putfield      #2                  // Field i:I

i++指令也包含了四個步驟,由於CPU按照時間片來進行執行緒排程的,只要是包含多個步驟的操作的執行,天然就是無法保證原子性的。因為這種執行緒執行,不像資料庫一樣可以回滾。如果一個執行緒要執行的步驟有5步,執行完3步就失去了CPU了,失去後就可能再也不會被排程。這怎麼可能保證原子性呢。
所以在以下兩個場景中可以使用volatile來代替synchronized:
1、運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒會修改變數的值。
2、變數不需要與其他狀態變數共同參與不變約束。

參考資料:
《深入理解java虛擬機器》周志明
https://blog.csdn.net/lc13571525583/article/details/90345760