JUC之volatile詳解

volatile

<mark>volatile是Java虛擬機提供的輕量級的同步機制,保證可見性袖订,不保證原子性痒玩,禁止指令重排(保證可序性)</mark>

Java內(nèi)存模型JMM

驗證可見性

public class VolatileDemo {

    public static void main(String[] args) {
        //volatile 可以保證可見性须误,及時通知其他線程至扰,主物理內(nèi)存的值已經(jīng)被修改
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            //暫停
            try {
                //確保后面的main線程啟動,進入循環(huán)
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addT060();
            System.out.println(Thread.currentThread().getName() + "\t update number value" + myData.number);
        }, "AAA").start();

        //第二個線程就是我們的main線程
        while (myData.number==0){
            //直到number不為0才退出循環(huán)
        }

        System.out.println(Thread.currentThread().getName()+"\t mission is over");


    }
}


class MyData {
    int number = 0;
    public void addT060() {
        this.number = 60;
    }
    
}

首先創(chuàng)建MyData類茬暇,定義一個int型的number變量值為0首昔,定義一個addT060()方法,將number值變成60糙俗。在從VolatileDemo中的主方法創(chuàng)建線程A勒奇,線程A被執(zhí)行調用后先暫停3秒,以取保此時的主線程已經(jīng)開始執(zhí)行while循環(huán)巧骚。3秒后A線程調用addT060()方法赊颠,將number值修改成60,A線程執(zhí)行結束劈彪。但是main線程中的number取依舊還是0竣蹦,而導致無法退出while循環(huán)!

運行查看結果~

通過畫圖可以清晰的看到main線程為啥會nmber值為60后沧奴,還處于while循環(huán)而導致的死循環(huán)痘括。這都是當前變量沒有可見性所導致的結果。

下面驗證volatile保證可見性,只需要在number變量中添加volatile關鍵字即可纲菌。

再次運行程序查看結果挠日。

線程A修改number的值后,就值寫入主內(nèi)存翰舌,main線程從主內(nèi)存中獲取最新的number值拷貝到工作內(nèi)存中嚣潜,此時nuber值不等于0main線程退出循環(huán),程序結束椅贱。

驗證原子性

原子性就是不可分割懂算,具有完整性,也即某個線程正在做某個具體業(yè)務時庇麦,中間不可以被加塞或者被分割计技。需要整體完整要么同時成功,要么同時失敗女器。

在上文中的MyData中添加addPlusPlus方法酸役。創(chuàng)建20個線程每個循環(huán)調用1000次addPlusPlus方法住诸。

public class VolatileDemo {

    public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        //需要等待前面20個線程全部執(zhí)行完驾胆,在用main線程取得最終的結果值是多少
        while (Thread.activeCount() > 2) {
            //main 線程 GC線程
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t int type final number value:" + myData.number);
    }
}


class MyData {

    volatile int number = 0;


    public void addT060() {
        this.number = 60;
    }

    //number加了volatile關鍵字
    public void addPlusPlus() {
        number++;
    }
}

運行查看結果~

number值大多不等于20000(存在等于20000情況),出現(xiàn)了數(shù)值丟失寫值的情況贱呐。所以volatite并不保證原子性丧诺!

例如現(xiàn)如今有兩個線程A和B,此時的number值為0奄薇。首先線程A被CPU調度執(zhí)行驳阎,被number值加1變成了1,正準備將工作內(nèi)存中的值同步更新到主內(nèi)存時馁蒂。CPU調度執(zhí)行了線程B呵晚,線程A被掛起,此時主內(nèi)存的number值還是為0沫屡,線程B將number值加一變成了1饵隙,并且成功的將number值同步更新到了主內(nèi)存中,主內(nèi)存中的值也變成了1沮脖。然后線程A被調度執(zhí)行繼續(xù)將工作內(nèi)存中更新的number值同步更新到主內(nèi)存中金矛,更新成功后當前主內(nèi)存中的最新的number值還是為1。這樣也就丟失了一次數(shù)值加一的操作勺届。

解決原子性

首先可以使用synchronized關鍵字驶俊,不過此操作太過重,不推薦免姿。其次我們可以使用JUC包下的atomic也就是java.util.concurrent.atomic

int類型的使用AtomicInteger

查看方法饼酿,使用getAndIncrement方法,將值自增加一胚膊。

添加如下代碼

public class VolatileDemo {

    public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                    myData.addMyAtomic();
                }
            }, String.valueOf(i)).start();
        }

        //需要等待前面20個線程全部執(zhí)行完故俐,在用main線程取得最終的結果值是多少
        while (Thread.activeCount() > 2) {
            //main 線程 GC線程
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t int type final number value:" + myData.number);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type final number value:" + myData.atomicInteger);
    }
}


class MyData {
    volatile int number = 0;

    public void addT060() {
        this.number = 60;
    }

    //number加了volatile關鍵字
    public void addPlusPlus() {
        number++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();

    public void addMyAtomic() {
        atomicInteger.getAndIncrement();
    }
}

運行查看結果~成功保證了原子性奈应!

指令重排

計算機在執(zhí)行程序時,為了提高性能购披,編譯器和處理器常常會對指令做重排杖挣,一般分以下3種:

  • 編譯器優(yōu)化的重排
  • 指令并行的重排
  • 內(nèi)存系統(tǒng)的重排

單線程環(huán)境里面確保程序最終執(zhí)行結果和代碼順序執(zhí)行的結果一致。

<mark>處理器在進行重排順序是必須要考慮指令之間的數(shù)據(jù)依賴性</mark>

多線程環(huán)境中線程交替執(zhí)行刚陡,由于編譯器優(yōu)化重排的存在惩妇,兩個線程中使用的變量能否保證一致性是無法確定的,導致結果無法預測筐乳!

所謂數(shù)據(jù)依賴性如圖

此代碼我們可以指令重排為1234歌殃,2134,1324蝙云。

但是我們能將語句4重排后變成第一個嗎氓皱?答案是不能因為語句4y = x * x必須要先聲明y和x,x的值也要是最終的勃刨。所以這就是存在數(shù)據(jù)依賴性波材,指令重排必須考慮數(shù)據(jù)依賴性否則會導致程序報錯或者最終結果不一致問題!

一般情況下是當先調用method1方法后身隐,語句1語句2一次執(zhí)行廷区。a變成1,flag變?yōu)閠rue贾铝。這時調用method2的線程才能執(zhí)行語句三將a變成6隙轻,打印結果。

但是在多線程的情況下垢揩,該兩個變量可能會被指令重排成語句2在語句1前被聲明玖绿,兩語句之間沒有數(shù)據(jù)依賴,所以存在這種情況叁巨。

此時在多線程的情況下斑匪,線程A調用執(zhí)行method1將flag變成了true后,還沒有繼續(xù)執(zhí)行俘种。線程B就被CPU調度執(zhí)行秤标,線程A掛起。線程B執(zhí)行method2宙刘,此時flag為true苍姜,執(zhí)行if語句中的代碼將a加5,此時a還為0悬包,所以0+5變成5衙猪,打印出來的a就變成了5。接著線程A繼續(xù)執(zhí)行a變成了1。

為了防止這種情況的出現(xiàn)垫释,所以我們要對關鍵變量禁止指令重排丝格!

<mark>volatile實現(xiàn)禁止指令重排優(yōu)化,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象棵譬。</mark>

在JVM底層volatile是采用“<mark>內(nèi)存屏障</mark>”來實現(xiàn)的显蝌。觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發(fā)現(xiàn),加入volatile關鍵字時订咸,會多出一個lock前綴指令曼尊,lock前綴指令實際上相當于一個內(nèi)存屏障(也成內(nèi)存柵欄),內(nèi)存屏障會提供3個功能:

  1. 它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置脏嚷,也不會把前面的指令排到內(nèi)存屏障的后面骆撇;即在執(zhí)行到內(nèi)存屏障這句指令時,在它前面的操作已經(jīng)全部完成.
  2. 它會強制將對緩存的修改操作立即寫入主存
  3. 如果是寫操作父叙,它會導致其他CPU中對應的緩存行無效神郊。

由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier內(nèi)存屏障則會告訴編譯器和CPU趾唱,不管什么指令都不能和這條Memory Barrier指令重排序涌乳,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重新排序優(yōu)化。內(nèi)存屏障另外一個作用是強制刷出各種CPU的緩存數(shù)據(jù)鲸匿,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本爷怀。

在對Volatile變量進行寫操作時阻肩,會在寫操作后加入一個store屏障指令带欢,將工作內(nèi)存中的共享變量值刷新回到主內(nèi)存。

對Volatile變量進行讀操作時烤惊,會在讀操作前加入一個load屏障指令出爹,從主內(nèi)存中讀取共享變量结耀。

單例模式volatile分析

單例模式詳解

public class SingletonDemo {

    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 我是構造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }

}

單線程下是可行的就不測試了,但是多線程下卻出現(xiàn)了問題!

為了解決不安全問題我們可以使用Double-Check雙重檢查來實現(xiàn)碍扔。在加入同步代碼塊之前和之后分別對是否存在實例對象進行判斷。

從而防止如果在多線程下鳄梅,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執(zhí)行巷送,另一個線程也通過了這個判斷語句,這時便會產(chǎn)生多個實例擂仍。

查看運行結果~

雖然目前還是成功的囤屹,但是還是存在問題的!也就是指令重排的問題逢渔。

原因在于某一個線程在執(zhí)行到第一次檢測,讀取到的instance不為null時,instance的引用對象 可能沒有完成初始化.

instance=new SingletonDem(); 可以分為以下步驟(偽代碼)

  • memory=allocate();//1.分配對象內(nèi)存空間

  • instance(memory);//2.初始化對象

  • instance=memory;//3.設置instance的指向剛分配的內(nèi)存地址,此時instance!=null

步驟2和步驟3不存在數(shù)據(jù)依賴關系.而且無論重排前還是重排后程序執(zhí)行的結果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的.

memory=allocate();//1.分配對象內(nèi)存空間

<mark>instance=memory;// 3 .設置instance的指向剛分配的內(nèi)存地址,此時instance!=null 但對象還沒有初始化完肋坚。會導致對象為空</mark>

instance(memory);// 2 .初始化對象

但是指令重排只會保證串行語義的執(zhí)行一致性(單線程) 并不會關心多線程間的語義一致性

所以當一條線程訪問instance不為null時,由于instance實例未必完成初始化,也就造成了線程安全問題。因此加入volatile可以禁止指令重排。

public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 我是構造方法SingletonDemo()");
    }

    //DCL 雙重檢查
    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 1000; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }

}

總結

工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象導致的可見性問題智厌,可以使用synchronizedvolatile關鍵字解決诲泌,他們都可以使用一個線程修改后的變量立即對其他線程可見。

對于指令重排導致的可見性問題和有序性問題铣鹏,可以利用volatile關鍵字解決敷扫,因為volatile的另外一個作用就是禁止指令重排序優(yōu)化。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诚卸,一起剝皮案震驚了整個濱河市呻澜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惨险,老刑警劉巖羹幸,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異辫愉,居然都是意外死亡栅受,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門恭朗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屏镊,“玉大人,你說我怎么就攤上這事痰腮《妫” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵膀值,是天一觀的道長棍丐。 經(jīng)常有香客問我,道長沧踏,這世上最難降的妖魔是什么歌逢? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮翘狱,結果婚禮上秘案,老公的妹妹穿的比我還像新娘。我一直安慰自己潦匈,他們只是感情好阱高,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著茬缩,像睡著了一般赤惊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寒屯,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天荐捻,我揣著相機與錄音黍少,去河邊找鬼。 笑死处面,一個胖子當著我的面吹牛厂置,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播魂角,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼昵济,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了野揪?” 一聲冷哼從身側響起访忿,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎斯稳,沒想到半個月后海铆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡挣惰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年卧斟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片憎茂。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡珍语,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出竖幔,到底是詐尸還是另有隱情板乙,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布拳氢,位于F島的核電站募逞,受9級特大地震影響,放射性物質發(fā)生泄漏饿幅。R本人自食惡果不足惜凡辱,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栗恩。 院中可真熱鬧,春花似錦洪燥、人聲如沸磕秤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽市咆。三九已至,卻和暖如春再来,著一層夾襖步出監(jiān)牢的瞬間蒙兰,已是汗流浹背磷瘤。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留搜变,地道東北人采缚。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像挠他,于是被迫代替她去往敵國和親扳抽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

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