volatile關(guān)鍵字的作用、原理

在只有雙重檢查鎖,沒有volatile的懶加載單例模式中,由于指令重排序的問題,我確實(shí)不會拿到兩個不同的單例了,但我會拿到“半個”單例

而發(fā)揮神奇作用的volatile,可以當(dāng)之無愧的被稱為Java并發(fā)編程中“出現(xiàn)頻率最高的關(guān)鍵字”,常用于保持內(nèi)存可見性和防止指令重排序。

保持內(nèi)存可見性

內(nèi)存可見性(Memory Visibility):所有線程都能看到共享內(nèi)存的最新狀態(tài)。

失效數(shù)據(jù)

以下是一個簡單的可變整數(shù)類:

public class MutableInteger {
    private int value;
    public int get(){
        return value;
    }
    public void set(int value){
        this.value = value;
    }
}

MutableInteger不是線程安全的,因?yàn)?code>get和set方法都是在沒有同步的情況下進(jìn)行的。如果線程1調(diào)用了set方法,那么正在調(diào)用的get的線程2可能會看到更新后的value值,也可能看不到

解決方法很簡單,將value聲明為volatile變量:

private volatile int value;

神奇的volatile關(guān)鍵字

神奇的volatile關(guān)鍵字解決了神奇的失效數(shù)據(jù)問題。

Java變量的讀寫

Java通過幾種原子操作完成工作內(nèi)存主內(nèi)存的交互:

  1. lock:作用于主內(nèi)存,把變量標(biāo)識為線程獨(dú)占狀態(tài)。
  2. unlock:作用于主內(nèi)存,解除獨(dú)占狀態(tài)。
  3. read:作用主內(nèi)存,把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存。
  4. load:作用于工作內(nèi)存,把read操作傳過來的變量值放入工作內(nèi)存的變量副本中。
  5. use:作用工作內(nèi)存,把工作內(nèi)存當(dāng)中的一個變量值傳給執(zhí)行引擎。
  6. assign:作用工作內(nèi)存,把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量。
  7. store:作用于工作內(nèi)存的變量,把工作內(nèi)存的一個變量的值傳送到主內(nèi)存中。
  8. write:作用于主內(nèi)存的變量,把store操作傳來的變量的值放入主內(nèi)存的變量中。

volatile如何保持內(nèi)存可見性

volatile的特殊規(guī)則就是:

  • read、load、use動作必須連續(xù)出現(xiàn)
  • assign、store、write動作必須連續(xù)出現(xiàn)

所以,使用volatile變量能夠保證:

  • 每次讀取前必須先從主內(nèi)存刷新最新的值。
  • 每次寫入后必須立即同步回主內(nèi)存當(dāng)中。

也就是說,volatile關(guān)鍵字修飾的變量看到的隨時是自己的最新值。線程1中對變量v的最新修改,對線程2是可見的。

防止指令重排

在基于偏序關(guān)系Happens-Before內(nèi)存模型中,指令重排技術(shù)大大提高了程序執(zhí)行效率,但同時也引入了一些問題。

一個指令重排的問題——被部分初始化的對象

懶加載單例模式和競態(tài)條件

一個懶加載單例模式實(shí)現(xiàn)如下:

class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance() {
        if ( instance == null ) { //這里存在競態(tài)條件
            instance = new Singleton();
        }
        return instance;
    }
}

競態(tài)條件會導(dǎo)致instance引用被多次賦值,使用戶得到兩個不同的單例。

DCL和被部分初始化的對象

為了解決這個問題,可以使用synchronized關(guān)鍵字將getInstance方法改為同步方法;但這樣串行化的單例是不能忍的。所以我猿族前輩設(shè)計(jì)了DCL(Double Check Lock,雙重檢查鎖)機(jī)制,使得大部分請求都不會進(jìn)入阻塞代碼塊:

class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance() {
        if ( instance == null ) { //當(dāng)instance不為null時,仍可能指向一個“被部分初始化的對象”
            synchronized (Singleton.class) {
                if ( instance == null ) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

“看起來”非常完美:既減少了阻塞,又避免了競態(tài)條件。不錯,但實(shí)際上仍然存在一個問題——當(dāng)instance不為null時,仍可能指向一個"被部分初始化的對象"

問題出在這行簡單的賦值語句:

instance = new Singleton();

它并不是一個原子操作。事實(shí)上,它可以”抽象“為下面幾條JVM指令:

memory = allocate();    //1:分配對象的內(nèi)存空間
initInstance(memory);   //2:初始化對象
instance = memory;      //3:設(shè)置instance指向剛分配的內(nèi)存地址

上面操作2依賴于操作1,但是操作3并不依賴于操作2,所以JVM可以以“優(yōu)化”為目的對它們進(jìn)行重排序,經(jīng)過重排序后如下:

memory = allocate();    //1:分配對象的內(nèi)存空間
instance = memory;      //3:設(shè)置instance指向剛分配的內(nèi)存地址(此時對象還未初始化)
ctorInstance(memory);   //2:初始化對象

可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向內(nèi)存memory時,這段嶄新的內(nèi)存還沒有初始化——即,引用instance指向了一個"被部分初始化的對象"。此時,如果另一個線程調(diào)用getInstance方法,由于instance已經(jīng)指向了一塊內(nèi)存空間,從而if條件判為false,方法返回instance引用,用戶得到了沒有完成初始化的“半個”單例。
解決這個該問題,只需要將instance聲明為volatile變量:

private static volatile Singleton instance;

也就是說,在只有DCL沒有volatile的懶加載單例模式中,仍然存在著并發(fā)陷阱。我確實(shí)不會拿到兩個不同的單例了,但我會拿到“半個”單例(未完成初始化)。
然而,許多面試書籍中,涉及懶加載的單例模式最多深入到DCL,卻只字不提volatile。這“看似聰明”的機(jī)制,曾經(jīng)被我廣大初入Java世界的猿胞大加吹捧——我在大四實(shí)習(xí)面試跟誰學(xué)的時候,也得意洋洋的從飽漢、餓漢講到Double Check,現(xiàn)在看來真是傻逼。對于考查并發(fā)的面試官而言,單例模式的實(shí)現(xiàn)就是一個很好的切入點(diǎn),看似考查設(shè)計(jì)模式,其實(shí)期望你從設(shè)計(jì)模式答到并發(fā)和內(nèi)存模型。

volatile如何防止指令重排

volatile關(guān)鍵字通過“內(nèi)存屏障”來防止指令被重排序。

為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。然而,對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能,為此,Java內(nèi)存模型采取保守策略。

下面是基于保守策略的JMM內(nèi)存屏障插入策略:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障。
  • 在每個volatile寫操作的后面插入一個StoreLoad屏障。
  • 在每個volatile讀操作的后面插入一個LoadLoad屏障。
  • 在每個volatile讀操作的后面插入一個LoadStore屏障。

進(jìn)階

在一次回答上述問題時,忘記了解釋一個很容易引起疑惑的問題:

如果存在這種重排序問題,那么synchronized代碼塊內(nèi)部不是也可能出現(xiàn)相同的問題嗎?

即這種情況:

class Singleton {
    ...
        if ( instance == null ) { //可能發(fā)生不期望的指令重排
            synchronized (Singleton.class) {
                if ( instance == null ) {
                    instance = new Singleton();
                    System.out.println(instance.toString()); //程序順序規(guī)則發(fā)揮效力的地方
                }
            }
        }
    ...
}

難道調(diào)用instance.toString()方法時,instance也可能未完成初始化嗎?

首先還請放寬心,synchronized代碼塊內(nèi)部雖然會重排序,但不會在代碼塊的范圍內(nèi)導(dǎo)致線程安全問題

Happens-Before內(nèi)存模型和程序順序規(guī)則

程序順序規(guī)則:如果程序中操作A在操作B之前,那么線程中操作A將在操作B之前執(zhí)行。

前面說過,只有在Happens-Before內(nèi)存模型中才會出現(xiàn)這樣的指令重排序問題。Happens-Before內(nèi)存模型維護(hù)了幾種Happens-Before規(guī)則,程序順序規(guī)則最基本的規(guī)則。程序順序規(guī)則的目標(biāo)對象是一段程序代碼中的兩個操作A、B,其保證此處的指令重排不會破壞操作A、B在代碼中的先后順序,但與不同代碼甚至不同線程中的順序無關(guān)

因此,在synchronized代碼塊內(nèi)部,instance = new Singleton()仍然會指令重排序,但重排序之后的所有指令,仍然能夠保證在instance.toString()之前執(zhí)行。進(jìn)一步的,單線程中,if ( instance == null )能保證在synchronized代碼塊之前執(zhí)行;但多線程中,線程1中的if ( instance == null )卻與線程2中的synchronized代碼塊之間沒有偏序關(guān)系,因此線程2中synchronized代碼塊內(nèi)部的指令重排對于線程1是不期望的,導(dǎo)致了此處的并發(fā)陷阱。

類似的Happens-Before規(guī)則還有volatile變量規(guī)則監(jiān)視器鎖規(guī)則等。程序猿可以借助(Piggyback)現(xiàn)有的Happens-Before規(guī)則來保持內(nèi)存可見性和防止指令重排。

注意點(diǎn)

上面簡單講解了volatile關(guān)鍵字的作用和原理,但對volatile的使用過程中很容易出現(xiàn)的一個問題是:

錯把volatile變量當(dāng)做原子變量。

出現(xiàn)這種誤解的原因,主要是volatile關(guān)鍵字使變量的讀、寫具有了“原子性”。然而這種原子性僅限于變量(包括引用)的讀和寫,無法涵蓋變量上的任何操作,即:

  • 基本類型的自增(如count++)等操作不是原子的。
  • 對象的任何非原子成員調(diào)用(包括成員變量成員方法)不是原子的。

如果希望上述操作也具有原子性,那么只能采取鎖、原子變量更多的措施。

總結(jié)

綜上,其實(shí)volatile保持內(nèi)存可見性和防止指令重排序的原理,本質(zhì)上是同一個問題,也都依靠內(nèi)存屏障得到解決。更多內(nèi)容請參見JVM相關(guān)書籍。


參考:


本文鏈接:volatile關(guān)鍵字的作用、原理
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容