volatile關鍵字的詳解-併發編程的體現

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

參考書籍:《Java高併發編程詳解》。尊重原創,支持知識付費,如下內容標記有摘抄的爲該書內容,如需查看該書的對應知識點,請購買原版書籍。編程

參考文章列表:緩存

---微信

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線程對initvalue進行了改變以後,咱們的Reader線程有效的觀察到了這個變量的變化,而且跟着輸出了Reader線程觀察到initvalue的結果。和上面不同的在於,這段代碼都能有效的結束程序

深刻了解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雖然可以保證併發編程有點三要素,可是會形成線程阻塞。

相關文章
相關標籤/搜索