并發(fā)情況下,單例模式之雙重檢驗(yàn)鎖陷阱

在我前面有寫過(guò)一篇關(guān)于單例模式的幾種創(chuàng)建的文章,最近在看多線程的時(shí)候,發(fā)現(xiàn)如果使用雙重檢驗(yàn)鎖則可能會(huì)發(fā)生問(wèn)題,接下來(lái)看我細(xì)細(xì)道來(lái)

單例模式的幾種創(chuàng)建方式文章地址:http://www.reibang.com/p/8ec72e016275

首先看一段代碼

public class SingletonV4 {

    private static SingletonV4 singletonV4;

    private SingletonV4() {
        System.out.println("--初始化--");
    }

    /**
     * 雙重檢驗(yàn)鎖
     * 能夠保證線程安全,且效率高
     * @return
     */
    public static SingletonV4 getInstance() {
        if (null == singletonV4) {
            synchronized (SingletonV4.class) {
                if (null == singletonV4) {
                    singletonV4 = new SingletonV4();
                }
            }
        }
        return singletonV4;
    }

    public static void main(String[] args) {
        SingletonV4 instance1 = SingletonV4.getInstance();
        SingletonV4 instance2 = SingletonV4.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}


--初始化--
com.dream.sunny.SingletonV4@1716361
com.dream.sunny.SingletonV4@1716361

如上是一段單例模式中的懶漢模式雙重檢驗(yàn)鎖,可能會(huì)有所疑惑,為什么需要兩次if判斷才進(jìn)行初始化對(duì)象
第一次if判斷主要是為了減少性能開(kāi)銷,之所以這么說(shuō),如果不加第一個(gè)if判斷,每次進(jìn)入getInstance()方法,synchronized關(guān)鍵字會(huì)將整個(gè)代碼進(jìn)行鎖住,加鎖操作,在進(jìn)行判斷是否已經(jīng)初始化,在進(jìn)行釋放鎖,加鎖和釋放鎖是有較大的性能開(kāi)銷,所以在最外層包裹一層if判斷實(shí)例是否被初始化,這樣就不會(huì)每次加鎖和釋放鎖了

既然synchronized鎖增加了性能開(kāi)銷,為什么要加鎖呢
當(dāng)然在單線程情況下,是沒(méi)有必要加鎖,而多線程情況下,多個(gè)線程同時(shí)進(jìn)行初始化對(duì)象操作,這樣就會(huì)有線程安全性問(wèn)題,為了防止這種情況,我們需要使用synchronized,這樣該方式在多線程情況下就是線程安全的

第二次if判斷目的在于有可能其他線程獲取過(guò)鎖学密,已經(jīng)初始化改變量镊折。第二次檢查還未通過(guò),才會(huì)真正初始化變量椭符。
這個(gè)方法檢查判定兩次剪勿,并使用鎖贸诚,所以形象稱為雙重檢查鎖定模式。
這個(gè)方案縮小鎖的范圍窗宦,減少鎖的開(kāi)銷赦颇,看起來(lái)很完美二鳄。然而這個(gè)方案有一些問(wèn)題卻很容易被忽略赴涵。

問(wèn)題點(diǎn):
這個(gè)被忽略的問(wèn)題在于 singletonV4 = new SingletonV4();在java中創(chuàng)建一個(gè)對(duì)象并非是一個(gè)原子操作,可以查看如下字節(jié)碼代碼

#創(chuàng)建一個(gè)新對(duì)象(創(chuàng)建 SingletonV4 對(duì)象實(shí)例,分配內(nèi)存)
19: new           #6                  // class com/dream/sunny/SingletonV4
#復(fù)制棧頂部一個(gè)字長(zhǎng)內(nèi)容(復(fù)制棧頂?shù)刂范┧希⒃賹⑵鋲喝霔m?
22: dup
#根據(jù)編譯時(shí)類型來(lái)調(diào)用實(shí)例方法(調(diào)用構(gòu)造器方法髓窜,初始化 SingletonV4 對(duì)象)
23: invokespecial #7                  // Method "<init>":()V
#設(shè)置類中靜態(tài)字段的值
26: putstatic     #5                  // Field singletonV4:Lcom/dream/sunny/SingletonV4;
#從局部變量0中裝載引用類型值(存入局部方法變量表)
29: aload_0

從字節(jié)碼中可以看到創(chuàng)建一個(gè)對(duì)象實(shí)例,大致可以分為以下幾步:

1.創(chuàng)建對(duì)象并分配內(nèi)存地址
2.調(diào)用構(gòu)造器方法,執(zhí)行初始化對(duì)象
3.將對(duì)象的引用地址賦值給變量

在多線程情況下,上面三個(gè)步驟可能會(huì)發(fā)生指令重排(在一些JIT編譯器中),編譯器或處理器會(huì)為了提高代碼性能效率,而改變代碼的執(zhí)行順序。
上面三個(gè)步驟2和3之間可能會(huì)發(fā)生重排,但是1不會(huì),因?yàn)?和3是要依托1指令的執(zhí)行結(jié)果,才能繼續(xù)往下走:

1.創(chuàng)建對(duì)象并分配內(nèi)存地址
2.將對(duì)象的引用地址賦值給變量
3.調(diào)用構(gòu)造器方法,執(zhí)行初始化對(duì)象

Java 語(yǔ)言規(guī)規(guī)定了線程執(zhí)行程序時(shí)需要遵守 intra-thread semantics欺殿。
不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變寄纵。
這個(gè)重排序在沒(méi)有改變單線程程序的執(zhí)行結(jié)果的前提下,可以提高程序的執(zhí)行性能脖苏。
雖然重排序并不影響單線程內(nèi)的執(zhí)行結(jié)果程拭,但是在多線程的環(huán)境就帶來(lái)一些問(wèn)題。

模擬兩個(gè)線程創(chuàng)建單例的場(chǎng)景,如下:

時(shí)間 線程A 線程B
t1 創(chuàng)建對(duì)象 ~
t2 分配內(nèi)存地址 ~
t3 ~ 判斷對(duì)象是否為空
t4 ~ 對(duì)象不為空,訪問(wèn)該對(duì)象
t5 初始化對(duì)象 ~
t6 訪問(wèn)該對(duì)象 ~

如果線程A獲取到鎖,進(jìn)入到創(chuàng)建對(duì)象實(shí)例,這個(gè)時(shí)候發(fā)生了指令重排,線程A執(zhí)行到t3時(shí)刻,此時(shí)線程B搶占了CPU執(zhí)行時(shí)間片,但是由于此時(shí)對(duì)象不為空,則直接返回對(duì)象出去,然而使用該對(duì)象卻發(fā)現(xiàn)該對(duì)象未被初始化就會(huì)報(bào)錯(cuò),并且從始至終,線程B無(wú)需獲取鎖

指令重排
前面已經(jīng)分析到,出現(xiàn)錯(cuò)誤的原因在于“指令重排”,那什么是指令重排呢?它什么在并發(fā)情況下指令重排會(huì)直接影響到程序的執(zhí)行結(jié)果呢?首先我們看一下“順序一致性內(nèi)存模型”概念棍潘。

順序一致性理論內(nèi)存模型
順序一致性內(nèi)存模型是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型恃鞋,它為程序員提供了極強(qiáng)的內(nèi)存可見(jiàn)性保證崖媚。順序一致性內(nèi)存模型有兩大特性:

  • 一個(gè)線程中的所有操作必須按照程序的順序來(lái)執(zhí)行。
  • (不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序恤浪。在順序一致性內(nèi)存模型中畅哑,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見(jiàn)。

實(shí)際JMM模型概念
但是水由,順序一致性模型只是一個(gè)理想化了的模型荠呐,在實(shí)際的JMM實(shí)現(xiàn)中,為了盡量提高程序運(yùn)行效率砂客,和理想的順序一致性內(nèi)存模型有以下差異:
在順序一致性模型中泥张,所有操作完全按程序的順序串行執(zhí)行。在JMM中不保證單線程操作會(huì)按程序順序執(zhí)行(即指令重排序)鞭盟。 順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序圾结,而JMM不保證所有線程能看到一致的操作執(zhí)行順序。 順序一致性模型保證對(duì)所有的內(nèi)存寫操作都具有原子性齿诉,而JMM不保證對(duì)64位的long型和double型變量的讀/寫操作具有原子性(分為2個(gè)32位寫操作進(jìn)行筝野,本文無(wú)關(guān)不細(xì)闡述)

什么是指令重排序
指令重排序是指編譯器或處理器為了優(yōu)化性能而采取的一種手段,在不存在數(shù)據(jù)依賴性情況下(如寫后讀粤剧,讀后寫歇竟,寫后寫),調(diào)整代碼執(zhí)行順序抵恋。 舉個(gè)例子:

int a = 1;
int b = 10;
int c = a * b

這段代碼C依賴于A,B焕议,但A,B沒(méi)有依賴關(guān)系弧关,所以代碼可能有2種執(zhí)行順序:

  • A->B->C
  • B->A->C 但無(wú)論哪種最終結(jié)果都一致盅安,這種滿足單線程內(nèi)無(wú)論如何重排序不改變最終結(jié)果的語(yǔ)義,被稱作as-if-serial語(yǔ)義世囊,遵守as-if-serial語(yǔ)義的編譯器别瞭,runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個(gè)幻覺(jué): 單線程程序是按程序的順序來(lái)執(zhí)行的。

雙重檢驗(yàn)鎖問(wèn)題解決方案
回頭看下我們出問(wèn)題的雙重檢查鎖程序株憾,它是滿足as-if-serial語(yǔ)義的嗎蝙寨?是的,單線程下它沒(méi)有任何問(wèn)題嗤瞎,但是在多線程下墙歪,會(huì)因?yàn)橹嘏判虺霈F(xiàn)問(wèn)題。
解決方案就是volatile關(guān)鍵字贝奇,對(duì)于volatile我們最深的印象是它保證了”可見(jiàn)性“虹菲,它的”可見(jiàn)性“是通過(guò)它的內(nèi)存語(yǔ)義實(shí)現(xiàn)的:

  • 寫volatile修飾的變量時(shí),JMM會(huì)把本地內(nèi)存中值刷新到主內(nèi)存
  • 讀volatile修飾的變量時(shí)掉瞳,JMM會(huì)設(shè)置本地內(nèi)存無(wú)效

重點(diǎn):為了實(shí)現(xiàn)可見(jiàn)性內(nèi)存語(yǔ)義毕源,編譯器在生成字節(jié)碼時(shí)髓帽,會(huì)在指令序列中插入內(nèi)存屏障來(lái)防止重排序!

volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制脑豹。volatile關(guān)鍵字有如下兩個(gè)作用

  • 保證被volatile修飾的共享變量對(duì)所有線程總數(shù)可見(jiàn)的郑藏,也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾共享變量的值,新值總是可以被其他線程立即得知瘩欺。
  • 禁止指令重排序優(yōu)化必盖。

由于 volatile 禁止對(duì)象創(chuàng)建時(shí)指令之間重排序,所以其他線程不會(huì)訪問(wèn)到一個(gè)未初始化的對(duì)象俱饿,從而保證安全性歌粥。

注意,volatile禁止指令重排序在 JDK 5 之后才被修復(fù)

對(duì)之前代碼加入volatile關(guān)鍵字拍埠,即可實(shí)現(xiàn)線程安全的單例模式失驶。

public class SingletonV4 {

    private static volatile SingletonV4 singletonV4;

    private SingletonV4() {
        System.out.println("--初始化--");
    }

    /**
     * 雙重檢驗(yàn)鎖
     * 能夠保證線程安全,且效率高
     * @return
     */
    public static SingletonV4 getInstance() {
        if (null == singletonV4) {
            synchronized (SingletonV4.class) {
                if (null == singletonV4) {
                    singletonV4 = new SingletonV4();
                }
            }
        }
        return singletonV4;
    }

    public static void main(String[] args) {
        SingletonV4 instance1 = SingletonV4.getInstance();
        SingletonV4 instance2 = SingletonV4.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}


--初始化--
com.dream.sunny.SingletonV4@1716361
com.dream.sunny.SingletonV4@1716361

總結(jié)
對(duì)象的創(chuàng)建可能發(fā)生指令的重排序,使用 volatile 可以禁止指令的重排序枣购,保證多線程環(huán)境內(nèi)的系統(tǒng)安全嬉探。

參考博客:https://www.cnblogs.com/lkxsnow/p/12293791.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市棉圈,隨后出現(xiàn)的幾起案子涩堤,更是在濱河造成了極大的恐慌,老刑警劉巖分瘾,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胎围,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡德召,警方通過(guò)查閱死者的電腦和手機(jī)白魂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)上岗,“玉大人福荸,你說(shuō)我怎么就攤上這事∫壕ィ” “怎么了逞姿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵辞嗡,是天一觀的道長(zhǎng)捆等。 經(jīng)常有香客問(wèn)我,道長(zhǎng)续室,這世上最難降的妖魔是什么栋烤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮挺狰,結(jié)果婚禮上明郭,老公的妹妹穿的比我還像新娘买窟。我一直安慰自己,他們只是感情好薯定,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布始绍。 她就那樣靜靜地躺著,像睡著了一般话侄。 火紅的嫁衣襯著肌膚如雪亏推。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天年堆,我揣著相機(jī)與錄音吞杭,去河邊找鬼。 笑死变丧,一個(gè)胖子當(dāng)著我的面吹牛芽狗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播痒蓬,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼童擎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了攻晒?” 一聲冷哼從身側(cè)響起柔昼,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炎辨,沒(méi)想到半個(gè)月后捕透,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碴萧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年乙嘀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片破喻。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡虎谢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出曹质,到底是詐尸還是另有隱情婴噩,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布羽德,位于F島的核電站几莽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宅静。R本人自食惡果不足惜章蚣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望姨夹。 院中可真熱鬧纤垂,春花似錦矾策、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至吼鱼,卻和暖如春榄鉴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蛉抓。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工庆尘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巷送。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓驶忌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親笑跛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子付魔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354