1. 程式人生 > >volatile關鍵字的詳解-併發程式設計的體現

volatile關鍵字的詳解-併發程式設計的體現

xl_echo編輯整理,歡迎轉載,轉載請宣告文章來源。歡迎新增echo微信(微訊號:t2421499075)交流學習。 百戰不敗,依不自稱常勝,百敗不頹,依能奮力前行。——這才是真正的堪稱強大!!


參考書籍:《Java高併發程式設計詳解》。尊重原創,支援知識付費,以下內容標記有摘抄的為該書內容,如需檢視該書的對應知識點,請購買原版書籍。

參考文章列表:

  • Volatile關鍵字介紹
  • 組成原理—記憶體及記憶體與CPU的關係
  • CPU中的cache結構以及cache一致性
  • 併發之volatile底層原理

volatile

什麼是volatile

volatile和synchronized相比,volatile被稱為輕量級鎖,並且能實現部分synchronized的語義。它在處理多執行緒併發的時候主要保證了共享資源的可見性,該功能可以理解為一個執行緒修改某一個共享變數的時候,另外一個變數可以讀到該共享變數的值。

資源無可見性在程式碼中產生的問題

基於瞭解程式原理就先上程式碼觀察結果的思想,我們可以通過觀察下面這段程式碼,先對volatile的基本體現有一個瞭解。

以下程式碼來自摘抄

public class VolatileFoo {

    final static int MAX = 5;

    static int init_value = 0;
    //static volatile int init_value = 0;

    public static void main(String[] args) {

        new Thread(() -> {
            int localValue = init_value;
            while (localValue < MAX) {
                if (init_value != localValue) {
                    System.out.printf("The init_value is update to [%d]\n", init_value);
                    localValue = init_value;
                }
            }
        }, "Reader").start();

        new Thread(() -> {
            int localValue = init_value;
            while (localValue < MAX) {
                System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
                init_value = localValue;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Updater").start();
    }

}

如果我們對volatile沒有了解的情況下,我們看到以上程式碼會覺得這就是實現建立了兩個執行緒不斷的去交替輸出一個變數的功能。觀看程式碼確實就是localValue不斷的變更,直到localValue等於5的時候,while迴圈才結束。但是我們可以看到以下結果,與這裡的結論不符,證明我們的結論有誤

實際的輸出結果是我們的Updater執行緒一直在輸出,當localValue=5的時候,我們的Updater執行緒就會結束迴圈。而我們的Reader執行緒卻一直在執行,並是處於死迴圈的狀態。這裡我們可以看到Reader執行緒讀到的localValue的值應該是一直沒有改變,也能夠明顯的看到一個問題,那就是兩個執行緒訪問一個變數,Updater修改變數的值後Reader執行緒並沒有獲取到這個值的變化。

volatile的初體驗

出現以上問題之後,我們可以看到共享變數的可見性它的重要性,解決上面程式的問題其實也比較簡單,只需要在上面做一個小修改即可。將init_value使用volatile進行修飾,其他的不變,我們再一次觀察輸出結果。

通過輸出結果我們可以看到,當Updater執行緒對init_value進行了改變之後,我們的Reader執行緒有效的觀察到了這個變數的變化,並且跟著輸出了Reader執行緒觀察到init_value的結果。和上面不一樣的在於,這段程式碼都能有效的結束程式

深入瞭解volatile,並有效掌握實現的方式還需要去了解->CPU硬體的執行和JMM記憶體模型。CPU與我們程式執行之間的關係是怎麼樣的,底層是怎麼出現這些錯誤的使我們瞭解volatile必不可少需要掌握的要點。

我們編寫的程式與CPU執行的關係

在我們的程式執行中,我們一個程式被CPU有效執行的一個過程要經過寫入硬碟,記憶體載入,CPU訪問執行。,硬碟、記憶體和CPU之間又有很大的區別。

  • 硬碟:儲存資料和軟體等資料的裝置,有容量大,斷電資料不丟失的特點。也被人們稱之為“資料倉庫”。我們編寫的程式就是儲存在硬盤裡面
  • 記憶體:1. 負責硬碟等硬體上的資料與CPU之間資料交換處理;2. 快取系統中的臨時資料。3. 斷電後資料丟失。可以稱為他就是硬碟和CPU之間的橋樑,並且我們的程式編寫完成儲存到硬碟中之後,開始執行就會被載入進入記憶體,並等待CPU對記憶體進行定址操作。
  • CPU:中央處理單元(Cntral Pocessing Uit)的縮寫,也叫處理器,是計算機的運算核心和控制核心。執行我們編寫的程式碼CPU只是接收到執行指令,然後對記憶體進行定址操作。

硬碟、記憶體和CPU的存取速度是遞增的,記憶體比硬碟要快很多,但是CPU又比記憶體塊很多倍,CPU的存取速度快到記憶體都跟不上,所以在CPU和記憶體之間出現了一個新的東西,那就是CPU Cache。cache的容量遠遠小於主存,因此出現cache miss在所難免,這也是我們為什麼會出現資料問題的關鍵所在。

CPU cache結構及cache操作資料導致資料不一致的問題

CPU中cache的結構我們可以開啟工作管理員,點選效能即可以看到。多核CPU的結構與單核相似,但是多了所有CPU共享的L3三級快取。在多核CPU的結構中,L1和L2是CPU私有的,L3則是所有CPU核心共享的。

在我們的系統中,由於短板效應,導致即時我們CPU速度再快也沒有辦法發揮它的能力。由於記憶體的讀取速度遠低於CPU,所以導致我們的程式執行速度,被記憶體限制。但是當cache出現之後完全改變了這個情況,它極大的增大了CPU的吞吐量。CPU只需要到cache中進行讀取和寫入操作即可,cache會在之後將結果同步到記憶體。但是當多執行緒情況下就會出現問題,每個執行緒都有自己的工作記憶體,本地記憶體,對應CPU中Cache。當多個執行緒同時操作一個變數的時候,都會進行讀取到CPU Cache當中,然後在同步會主記憶體,這樣就會導致資料結果不一致。也就是我們平時看到的,多執行緒結果與預期不一致的問題。

Java記憶體模型

Java的記憶體模型(Java Memory Mode)制定了Java虛擬機器如何與計算機的主存進行工作,理解Java記憶體模型對於編寫併發程式非常重要。在CPU cache當中我們使用文字描述了多執行緒情況下出現結果不一致情況,這裡我們可以通過Java記憶體模型的圖解來更直觀的看到這個情況是怎麼出現的。

圖中執行緒1的工作記憶體和執行緒2的工作記憶體就是我們上面描述的當有多個執行緒操作一個變數時,每個執行緒就會將變數複製一份到自己的工作記憶體當中。當我們的多執行緒執行的時候,每一個執行緒賦值一份變數,都對值進行修改,當共享變數不可見的時候,最終就會導致結果不一致。

併發程式設計的三個重要特性

  • 原子性
    • 原型性是指一個操作的完整性,要麼該操作改變的值或者資源全部成功,要麼全部不成功。
  • 有序性
    • 所謂有序性就是指程式碼在執行過程當中的先後順序。
  • 可見性
    • 可見性在我們最上面的例子裡面就展現了,就是一個執行緒修改共享變數的值的時候,另外一個執行緒能夠看到這個變數的值被改變。

在我們多執行緒併發程式設計當中,它的三大特性是保證併發執行不出現錯誤的關鍵,volatile我們目前能夠看到在併發程式設計當中能夠保證可見性。除了可見性外還其實它還可以保證有序性,只是不能保證原子性而已。假若能夠保證原子性,它和synchronize的作用基本那就是一樣的,只是底層的實現原理不一樣而已。

volatile如何保證有序性(摘抄)

volatile關鍵字對順序性的保證就比較霸道,直接禁止JVM和處理器對volatile關鍵字修飾的指令重新排序,但是對於volatile前後無依賴關係的指令則可以隨便怎麼排序。

volatile可見性的底層實現原理

volatile底層的實現其實是通過lock關鍵字進行實現的,我們可以去獲取class的彙編碼,當使用volatile修飾和不使用volatile的程式碼分別獲取到class的彙編碼,然後進行對比,你會發現標有volatile的變數在進行寫操作時,會在前面加上lock質量字首。而lock指令字首會做如下兩件事

  • 將當前處理器快取行的資料寫回到記憶體。lock指令字首在執行指令的期間,會產生一個lock訊號,lock訊號會保證在該訊號期間會獨佔任何共享記憶體。lock訊號一般不鎖匯流排,而是鎖快取。因為鎖匯流排的開銷會很大。
  • 將快取行的資料寫回到記憶體的操作會使得其他CPU快取了該地址的資料無效。

volatile和synchronize的區別

  • volatile只能修飾例項變數或者類變數,synchronize只能修飾方法或者語句塊
  • volatile無法保證原子性,synchronize能夠保證原子性
  • volatile和synchronize都能保證有序性,只是實現方式不一樣
  • volatile不會使執行緒陷入阻塞,synchronize相反

總結

volatile被稱為輕量級的synchronize是因為他能夠有效的實現併發程式設計的有序性和可見性。但是同時它有自己的缺點,比如不能保證原子性的問題。部分場景能夠直接volatile,比如對執行緒的喚起和關閉。synchronize雖然能夠保證併發程式設計有點三要素,但是會造成執行緒阻塞