volatile原理詳解

理論

線程并發(fā)執(zhí)行下的原子性担扑、可見(jiàn)性、順序性問(wèn)題

  1. 原子性:線程上下文切換導(dǎo)致指令交錯(cuò)執(zhí)行而產(chǎn)生代碼執(zhí)行非原子性問(wèn)題鸭丛,可以通過(guò)加synchorized鎖來(lái)保障原子性罐监,線程1獲取到鎖之后,線程2就不能在執(zhí)行臨界區(qū)中的代碼贯吓,需要等待線程1釋放鎖之后線程2獲取鎖才可以執(zhí)行臨界區(qū)代碼懈凹,臨界區(qū)代碼始終只能有一個(gè)線程在執(zhí)行,從而保障原子性悄谐。
  2. 可見(jiàn)性:java內(nèi)存模型是由主內(nèi)存和工作內(nèi)存組成的介评,主內(nèi)存中存放的變量是所有線程都可以訪問(wèn)的,工作內(nèi)存中的變量是每個(gè)線程獨(dú)有的爬舰。線程從主內(nèi)存中取出變量后會(huì)在自己的工作內(nèi)存中做緩存们陆,緩存之后線程再訪問(wèn)數(shù)據(jù)就會(huì)從自己的緩存中取數(shù)據(jù)寒瓦,如果另外一個(gè)線程對(duì)主內(nèi)存中的變量進(jìn)行寫操作可能不會(huì)把修改之后的變量同步到其它線程的工作內(nèi)存中從而導(dǎo)致線程的不可見(jiàn)性∑撼穑可以通過(guò)加鎖或者加volatile關(guān)鍵字來(lái)保障變量的可見(jiàn)性孵构。
  3. 有序性:cpu為了提升指令執(zhí)行的吞吐量,會(huì)使用并發(fā)的方式執(zhí)行指令烟很,為了提升指令的并發(fā)執(zhí)行效率會(huì)對(duì)指令進(jìn)行重排序颈墅。指令重排之后會(huì)導(dǎo)致代碼的執(zhí)行順序被打亂,如果多線程交錯(cuò)執(zhí)行指令時(shí)就會(huì)導(dǎo)致一些問(wèn)題雾袱⌒羯福可以通過(guò)添加volatile的方式防止指令重排。synchorized也可以保障代碼的有序性芹橡,但是不能阻止臨界區(qū)內(nèi)的指令重排毒坛,前提是成員變量要完全交給synchorized管理。

volatile原理

volatile 可以保障指令執(zhí)行的可見(jiàn)性和有序性林说,變量如果用volatile修飾后會(huì)煎殷,如果對(duì)變量進(jìn)行讀操作時(shí)會(huì)加上讀屏障讀屏障下面的代碼直接從主內(nèi)存中讀數(shù)據(jù)并且讀屏障下面的代碼不會(huì)被重排到讀屏障上面。對(duì)變量進(jìn)行寫操作時(shí)會(huì)加上寫屏障腿箩,寫屏障上面的代碼會(huì)直接把變量寫到主存中并且寫屏障上面的代碼不會(huì)被重排到寫屏障下面豪直。從而可以保障指令執(zhí)行的可見(jiàn)性和有序性。

volatile只有一個(gè)線程執(zhí)行寫操作珠移,其它單個(gè)或多個(gè)線程執(zhí)行讀操作的情況弓乙,如果存在多個(gè)線程對(duì)添加volatile修飾的變量進(jìn)行寫操作還有可能會(huì)存在臟讀等問(wèn)題。

代碼解析

案例一

單例模式j(luò)ava代碼

    public final class Singleton {
        private Singleton() { }
        private static Singleton INSTANCE = null;
        public static Singleton getInstance() {
            if(INSTANCE == null) { // t2
                // 首次訪問(wèn)會(huì)同步钧惧,而之后的使用沒(méi)有 synchronized
                synchronized(Singleton.class) {
                    if (INSTANCE == null) { // t1
                        INSTANCE = new Singleton();
                    }
                }
            }
            return INSTANCE;
        }
    }

單例模式j(luò)ava字節(jié)碼解析(上面的java代碼編譯后的字節(jié)碼)


有序性分析.png
  • 17行 在椣救停空間創(chuàng)建新的對(duì)象的引用變量
  • 20行 復(fù)制一份新建的對(duì)象的引用變量
  • 21行 復(fù)制的引用變量調(diào)用構(gòu)造方法
  • 24行 新建的引用變量復(fù)制給靜態(tài)成員變量
  • 也許jvm會(huì)對(duì)指令進(jìn)行優(yōu)化優(yōu)化后21行和24行的執(zhí)行順序調(diào)換。
有序性分析-2.png

如上圖如果字節(jié)碼執(zhí)行時(shí)發(fā)生指令重排21行和24行的執(zhí)行順序發(fā)生調(diào)換浓瞪,假如現(xiàn)在有一個(gè)線程t1執(zhí)行到了引用變量賦值給靜態(tài)變量INSTANCE但是懈玻,引用變量還沒(méi)來(lái)得及調(diào)用構(gòu)造方法創(chuàng)建對(duì)象,此時(shí)發(fā)生了上下文切換乾颁,t2線程執(zhí)行g(shù)etInstance方法判斷INSTANCE已經(jīng)不為NULL了就會(huì)直接返回,但是事實(shí)上INSTANCE賦值的對(duì)象還沒(méi)來(lái)得及調(diào)用構(gòu)造方法創(chuàng)建對(duì)象钮孵。所以返回的對(duì)象用起來(lái)就會(huì)出現(xiàn)問(wèn)題。

案例二

指令重排證明及解決方案

    int num = 0;
    boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

上面代碼通過(guò)測(cè)試工具測(cè)試結(jié)果如下圖


有序性分析-3.png

如上圖按照正常運(yùn)行就算是多線程并發(fā)執(zhí)行這段代碼所能出現(xiàn)的結(jié)果可能就是1或者4但是實(shí)際運(yùn)行過(guò)程中還會(huì)存在一種可能就是0历涝,那么什么時(shí)候會(huì)出現(xiàn)0的結(jié)果呢,如果發(fā)生指令重排導(dǎo)致num=2;和ready=true;這兩行代碼的執(zhí)行順序交換然后就會(huì)可能出現(xiàn)結(jié)果為0的情況。

給ready 成員變量添加volatile修飾測(cè)試的結(jié)果就不會(huì)再出現(xiàn)0的結(jié)果荧库,因?yàn)閷?duì) volatile 變量的寫指令后會(huì)加入寫屏障對(duì)volatile變量的讀指令前會(huì)加入讀屏障。

    int num = 0;
    volatile boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

volatile修飾過(guò)的變量添加了都屏障和寫屏障所以可以保障指令執(zhí)行時(shí)的有序性


有序性分析-4.png

happens-before

happerns-before指的是一個(gè)線程對(duì)共享變量的寫操作對(duì)于其它線程讀共享變量都是可見(jiàn)的分衫,一共有7種情況,如果不屬于這7種情況的共享變量的讀操作都是不可見(jiàn)的蚪战。

單例實(shí)現(xiàn)問(wèn)題分析

實(shí)現(xiàn)1

    // 問(wèn)題1:為什么加 final
    // 問(wèn)題2:如果實(shí)現(xiàn)了序列化接口, 還要做什么來(lái)防止反序列化破壞單例
    public final class Singleton implements Serializable {
        // 問(wèn)題3:為什么設(shè)置為私有? 是否能防止反射創(chuàng)建新的實(shí)例?
        private Singleton() {}
        // 問(wèn)題4:這樣初始化是否能保證單例對(duì)象創(chuàng)建時(shí)的線程安全?
        private static final Singleton INSTANCE = new Singleton();
        // 問(wèn)題5:為什么提供靜態(tài)方法而不是直接將 INSTANCE 設(shè)置為 public, 說(shuō)出你知道的理由
        public static Singleton getInstance() {
            return INSTANCE;
        }
        public Object readResolve() {
            return INSTANCE;
        }
    }

  1. 添加final是為了保障單例類不會(huì)被其它類繼承然后修改方法創(chuàng)建多個(gè)對(duì)象牵现。
  2. 通過(guò)readResolve方法來(lái)防止反序列化破壞單例。
  3. 不能繁殖反射創(chuàng)建新的實(shí)例
  4. 可以保障單例對(duì)象創(chuàng)建時(shí)的線程安全邀桑,因?yàn)閟tatic修飾的變量默認(rèn)是jvm加載類對(duì)象時(shí)創(chuàng)建對(duì)象,jvm可以保障創(chuàng)建單例對(duì)象的線程安全性壁畸。
  5. 面向?qū)ο蟮乃枷耄梢栽讷@取INSTANCE時(shí)添加一些自己的實(shí)現(xiàn)捏萍。

實(shí)現(xiàn)2

    // 問(wèn)題1:枚舉單例是如何限制實(shí)例個(gè)數(shù)的
    // 問(wèn)題2:枚舉單例在創(chuàng)建時(shí)是否有并發(fā)問(wèn)題
    // 問(wèn)題3:枚舉單例能否被反射破壞單例
    // 問(wèn)題4:枚舉單例能否被反序列化破壞單例
    // 問(wèn)題5:枚舉單例屬于懶漢式還是餓漢式
    // 問(wèn)題6:枚舉單例如果希望加入一些單例創(chuàng)建時(shí)的初始化邏輯該如何做
    enum Singleton {
        INSTANCE;
    }
單例模式字節(jié)碼-1.png
  1. 由上圖字節(jié)碼可以看到枚舉類型編譯成字節(jié)碼后實(shí)際上也是在自己的類內(nèi)部新建了一個(gè)靜態(tài)變量,靜態(tài)變量會(huì)在jvm 加載類是創(chuàng)建對(duì)象并且復(fù)制給靜態(tài)變量走敌,所以由jvm保障單例創(chuàng)建的安全性这揣。
  2. 沒(méi)有并發(fā)問(wèn)題悔常。
  3. 不能被反射破壞單例给赞。
  4. 不能被反序列化破壞單例矫户。
  5. 屬于餓漢式。
  6. 可以通過(guò)布爾類型的構(gòu)造方法柑蛇。

實(shí)現(xiàn)3

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    // 分析這里的線程安全, 并說(shuō)明有什么缺點(diǎn)
    public static synchronized Singleton getInstance() {
        if( INSTANCE != null ){
            return INSTANCE;
        }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

是線程安全的,缺點(diǎn)是線程每次獲取單例對(duì)象都要獲取鎖和釋放鎖會(huì)造成系統(tǒng)資源的浪費(fèi)耻台。

實(shí)現(xiàn)4

    public final class Singleton {
    
        private Singleton() { }
        
        // 問(wèn)題1:解釋為什么要加 volatile ?
        private static volatile Singleton INSTANCE = null;

        // 問(wèn)題2:對(duì)比實(shí)現(xiàn)3, 說(shuō)出這樣做的意義
        public static Singleton getInstance() {
            if (INSTANCE != null) {
                return INSTANCE;
            }
            synchronized (Singleton.class) {
                // 問(wèn)題3:為什么還要在這里加為空判斷, 之前不是判斷過(guò)了嗎
                if (INSTANCE != null) { // t2
                    return INSTANCE;
                }
                INSTANCE = new Singleton();
                return INSTANCE;
            }
        }
    }
  1. 加volatile為了保障可見(jiàn)性和有序性
  2. 跟實(shí)現(xiàn)3相比縮小了鎖臨界區(qū)的范圍空另,如果單例對(duì)象已經(jīng)創(chuàng)建成功了其它線程再獲取實(shí)例時(shí)就不會(huì)再獲取一次鎖,可以提升程序執(zhí)行的效率。
  3. 如果多線程并發(fā)執(zhí)行時(shí)如果t1獲取鎖但是還未創(chuàng)建單例對(duì)象成功時(shí)發(fā)生上下文切換坝咐,t2線程也獲取鎖并且進(jìn)入blocked狀態(tài)析恢。t1執(zhí)行完之后t2獲取到鎖并且執(zhí)行時(shí)如果沒(méi)有為空判斷會(huì)再一次創(chuàng)建單例對(duì)象。

實(shí)現(xiàn)5

    public final class Singleton {
        private Singleton() { }
        // 問(wèn)題1:屬于懶漢式還是餓漢式
        private static class LazyHolder {
            static final Singleton INSTANCE = new Singleton();
        }
        // 問(wèn)題2:在創(chuàng)建時(shí)是否有并發(fā)問(wèn)題
        public static Singleton getInstance() {
            return LazyHolder.INSTANCE;
        }
    }
  1. 屬于懶漢式泽篮,jvm加載class類時(shí)并不是一上來(lái)就加載class類而是用時(shí)才會(huì)加載,所以在jvm加載singleton時(shí)并不會(huì)立刻加載LazyHolder柑船,所以基于這個(gè)原理實(shí)現(xiàn)懶漢式創(chuàng)建單例對(duì)象的。
  2. 創(chuàng)建時(shí)沒(méi)有并發(fā)問(wèn)題椎组,因?yàn)閖vm加載LazyHolder內(nèi)部類時(shí)會(huì)初始化靜態(tài)變量并且給靜態(tài)變量賦值,jvm會(huì)保障類加載時(shí)初始化靜態(tài)變量的線程安全性专筷。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒸苇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子溪烤,更是在濱河造成了極大的恐慌,老刑警劉巖檬嘀,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異掂铐,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)全陨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門衷掷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人雨涛,你說(shuō)我怎么就攤上這事【迪ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵旧困,是天一觀的道長(zhǎng)稼锅。 經(jīng)常有香客問(wèn)我,道長(zhǎng)矩距,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任陡蝇,我火速辦了婚禮哮肚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘允趟。我一直安慰自己,他們只是感情好涣楷,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布抗碰。 她就那樣靜靜地躺著狮斗,像睡著了一般改含。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天鹃觉,我揣著相機(jī)與錄音睹逃,去河邊找鬼盗扇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛佑笋,可吹牛的內(nèi)容都是我干的斑鼻。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼蜀备,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼荒叶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起些楣,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚕钦,沒(méi)想到半個(gè)月后埋市,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡道宅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年污茵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泞当。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡襟士,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出陋桂,到底是詐尸還是另有隱情,我是刑警寧澤嗜历,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站痕囱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏鞍恢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一抹腿、第九天 我趴在偏房一處隱蔽的房頂上張望旭寿。 院中可真熱鬧,春花似錦盅称、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至痛黎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掖蛤,已是汗流浹背井厌。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仅仆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓拳魁,卻偏偏與公主長(zhǎng)得像撮弧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贿衍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

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