大數據成神之路-Java高級特性加強(volatile關鍵字)

請戳GitHub原文: https://github.com/wangzhiwub...

大數據成神之路系列:

請戳GitHub原文: https://github.com/wangzhiwub...java

Java高級特性加強-集合git

Java高級特性加強-多線程程序員

Java高級特性加強-Synchronizedgithub

Java高級特性加強-volatile面試

Java高級特性加強-併發集合框架編程

Java高級特性加強-分佈式緩存

Java高級特性加強-Zookeeper安全

Java高級特性加強-JVM網絡

Java高級特性加強-NIO多線程

公衆號

  • 全網惟一一個從0開始幫助Java開發者轉作大數據領域的公衆號~
  • 公衆號大數據技術與架構或者搜索import_bigdata關注,大數據學習路線最新更新,已經有不少小夥伴加入了~

Java高級特性加強-Volatile

本部分網絡上有大量的資源能夠參考,在這裏作了部分整理,感謝前輩的付出,每節文章末尾有引用列表,源碼推薦看JDK1.8之後的版本,注意甄別~

多線程

集合框架

NIO

Java併發容器

    • *

volatile關鍵字

volatile特性

volatile就能夠說是java虛擬機提供的最輕量級的同步機制。但它同時不容易被正確理解,也至於在併發編程中不少程序員遇到線程安全的問題就會使用synchronized。Java內存模型告訴咱們,各個線程會將共享變量從主內存中拷貝到工做內存,而後執行引擎會基於工做內存中的數據進行操做處理。線程在工做內存進行操做後什麼時候會寫到主內存中?這個時機對普通變量是沒有規定的,而針對volatile修飾的變量給java虛擬機特殊的約定,線程對volatile變量的修改會馬上被其餘線程所感知,即不會出現數據髒讀的現象,從而保證數據的「可見性」。
通俗來講就是,線程A對一個volatile變量的修改,對於其它線程來講是可見的,即線程每次獲取volatile變量的值都是最新的。

volatile的實現原理

在生成彙編代碼時會在volatile修飾的共享變量進行寫操做的時候會多出Lock前綴的指令。咱們想這個Lock指令確定有神奇的地方,那麼Lock前綴的指令在多核處理器下會發現什麼事情了?主要有這兩個方面的影響:

將當前處理器緩存行的數據寫回系統內存;
這個寫回內存的操做會使得其餘CPU裏緩存了該內存地址的數據無效

爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。所以,通過分析咱們能夠得出以下結論:

Lock前綴的指令會引發處理器緩存寫回內存;
一個處理器的緩存回寫到內存會致使其餘處理器的緩存失效;
當處理器發現本地緩存失效後,就會從內存中重讀該變量數據,便可以獲取當前最新值。

這樣針對volatile變量經過這樣的機制就使得每一個線程都能得到該變量的最新值。

咱們在項目中如何使用?

一、狀態標記量
在高併發的場景中,經過一個boolean類型的變量isopen,控制代碼是否走促銷邏輯,該如何實現?

public class ServerHandler {
    private volatile isopen;
    public void run() {
        if (isopen) {
           //isopen=true邏輯
        } else {
          //其餘邏輯
        }
    }
    public void setIsopen(boolean isopen) {
        this.isopen = isopen
    }
}

場景細節無需過度糾結,這裏只是舉個例子說明volatile的使用方法,用戶的請求線程執行run方法,若是須要開啓促銷活動,能夠經過後臺設置,具體實現能夠發送一個請求,調用setIsopen方法並設置isopen爲true,因爲isopen是volatile修飾的,因此一經修改,其餘線程均可以拿到isopen的最新值,用戶請求就能夠執行isopen=true的邏輯。

二、double check
單例模式的一種實現方式,但不少人會忽略volatile關鍵字,由於沒有該關鍵字,程序也能夠很好的運行,只不過代碼的穩定性總不是100%,說不定在將來的某個時刻,隱藏的bug就出來了。

class Singleton {
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    } 
}

不過在衆多單例模式的實現中,我比較推薦懶加載的優雅寫法Initialization on Demand Holder(IODH)。

public class Singleton {  
    static class SingletonHolder {  
        static Singleton instance = new Singleton();  
    }  
      
    public static Singleton getInstance(){  
        return SingletonHolder.instance;  
    }  
}

如何保證內存可見性

在java虛擬機的內存模型中,有主內存和工做內存的概念,每一個線程對應一個工做內存,並共享主內存的數據,下面看看操做普通變量和volatile變量有什麼不一樣:
一、對於普通變量:讀操做會優先讀取工做內存的數據,若是工做內存中不存在,則從主內存中拷貝一份數據到工做內存中;寫操做只會修改工做內存的副本數據,這種狀況下,其它線程就沒法讀取變量的最新值。
二、對於volatile變量,讀操做時JMM會把工做內存中對應的值設爲無效,要求線程從主內存中讀取數據;寫操做時JMM會把工做內存中對應的數據刷新到主內存中,這種狀況下,其它線程就能夠讀取變量的最新值。
volatile變量的內存可見性是基於內存屏障(Memory Barrier)實現的,什麼是內存屏障?內存屏障,又稱內存柵欄,是一個CPU指令。在程序運行時,爲了提升執行性能,編譯器和處理器會對指令進行重排序,JMM爲了保證在不一樣的編譯器和CPU上有相同的結果,經過插入特定類型的內存屏障來禁止特定類型的編譯器重排序和處理器重排序,插入一條內存屏障會告訴編譯器和CPU:無論什麼指令都不能和這條Memory Barrier指令重排序。

舉例以下:

class Singleton {
    private volatile static Singleton instance;
    private int a;
    private int b;
    private int b;
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    a = 1;  // 1
                     b = 2;  // 2
                    instance = new Singleton();  // 3
                    c = a + b;  // 4
                }
            }
        }
        return instance;
    } 
}

一、若是變量instance沒有volatile修飾,語句一、二、3能夠隨意的進行重排序執行,即指令執行過程多是3214或1324。
二、若是是volatile修飾的變量instance,會在語句3的先後各插入一個內存屏障。
經過觀察volatile變量和普通變量所生成的彙編代碼能夠發現,操做volatile變量會多出一個lock前綴指令:

Java代碼:
instance = new Singleton();

彙編代碼:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: **lock** addl $0x0,(%esp);

這個lock前綴指令至關於上述的內存屏障,提供瞭如下保證:
一、將當前CPU緩存行的數據寫回到主內存;
二、這個寫回內存的操做會致使在其它CPU裏緩存了該內存地址的數據無效。
CPU爲了提升處理性能,並不直接和內存進行通訊,而是將內存的數據讀取到內部緩存(L1,L2)再進行操做,但操做完並不能肯定什麼時候寫回到內存,若是對volatile變量進行寫操做,當CPU執行到Lock前綴指令時,會將這個變量所在緩存行的數據寫回到內存,不過仍是存在一個問題,就算內存的數據是最新的,其它CPU緩存的仍是舊值,因此爲了保證各個CPU的緩存一致性,每一個CPU經過嗅探在總線上傳播的數據來檢查本身緩存的數據有效性,當發現本身緩存行對應的內存地址的數據被修改,就會將該緩存行設置成無效狀態,當CPU讀取該變量時,發現所在的緩存行被設置爲無效,就會從新從內存中讀取數據到緩存中。
這也是咱們以前講的原理部分的解釋~

volatile的happens-before關係

volatile變量能夠經過緩存一致性協議保證每一個線程都能得到最新值,即知足數據的「可見性」。咱們繼續延續上一篇分析問題的方式(我一直認爲思考問題的方式是屬於本身,也纔是最重要的,也在不斷培養這方面的能力),我一直將併發分析的切入點分爲兩個核心,三大性質。兩大核心:JMM內存模型(主內存和工做內存)以及happens-before;三條性質:原子性,可見性,有序性(關於三大性質的總結在之後得文章會和你們共同探討)。廢話很少說,先來看兩個核心之一:volatile的happens-before關係。
在六條happens-before規則中有一條是:volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。下面咱們結合具體的代碼,咱們利用這條規則推導下:

public class VolatileExample {
    private int a = 0;
    private volatile boolean flag = false;
    public void writer(){
        a = 1;          //1
        flag = true;   //2
    }
    public void reader(){
        if(flag){      //3
            int i = a; //4
        }
    }
}

上面的實例代碼對應的happens-before關係以下圖所示:
ab3fc4589fa61bf75ad91d7080664a7d.png
加鎖線程A先執行writer方法,而後線程B執行reader方法圖中每個箭頭兩個節點就代碼一個happens-before關係,黑色的表明根據程序順序規則推導出來,紅色的是根據volatile變量的寫happens-before 於任意後續對volatile變量的讀,而藍色的就是根據傳遞性規則推導出來的。這裏的2 happen-before 3,一樣根據happens-before規則定義:若是A happens-before B,則A的執行結果對B可見,而且A的執行順序先於B的執行順序,咱們能夠知道操做2執行結果對操做3來講是可見的,也就是說當線程A將volatile變量 flag更改成true後線程B就可以迅速感知。


參考文章和書籍:

《Java併發編程的藝術》
《實戰Java高併發程序設計》
https://blog.csdn.net/qq_3433...
https://www.jianshu.com/p/a5f...
https://www.jianshu.com/p/506...


Github:

關注公衆號,內推,面試,資源下載,關注更多大數據技術~
                   預計更新500+篇文章,已經更新50+篇~
相關文章
相關標籤/搜索