volatile關(guān)鍵字

volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制

具有三大特性:

  1. 保證可見性
  2. 不保證原子性
  3. 禁止指令重排

要想講清楚這三大特性逾滥,首先要了解JMM

JMM

JMM(Java內(nèi)存模型 Java Memory Model)是一種抽象概念雄人,描述的是一組規(guī)則或規(guī)范降盹,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段谦纱,靜態(tài)字段和構(gòu)成數(shù)組的對(duì)象)的訪問方式

JMM關(guān)于同步的規(guī)定:

  1. 線程解鎖前孤澎,必須把共享變量的值刷新回主內(nèi)存
  2. 線程加鎖前新锈,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存
  3. 加鎖解鎖是同一把鎖

JVM會(huì)為每個(gè)線程開辟獨(dú)立的工作內(nèi)存(或稱為椇觊牛空間)保存線程私有數(shù)據(jù)捐祠,所有變量都保存在主內(nèi)存中碱鳞,線程對(duì)變量的操作必須在工作內(nèi)存中完成,首先將變量從主內(nèi)存拷貝至工作內(nèi)存踱蛀,然后對(duì)變量進(jìn)行操作窿给,完成后再將變量寫回主內(nèi)存,線程間無法訪問對(duì)方的工作內(nèi)存率拒,線程間通信必須通過主內(nèi)存來完成

JMM的三大特性:

  1. 可見性
  2. 原子性
  3. 有序性

volatile滿足JMM三大特性的兩點(diǎn)

可見性

例:

public class VolatileDemo {

    public static void main(String[] args) {

        visibility();

    }

    private static void visibility() {

        Data data = new Data();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " begin");

            // 等待3秒后更新data.num
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.add(10);
            System.out.println(Thread.currentThread().getName() + " updated num: " + data.num);
        }, "thread-1").start();

        // 主線程檢測(cè)當(dāng)data.num不為0時(shí)崩泡,結(jié)束循環(huán),否則一直等待
        while (data.num == 0) {

        }

        System.out.println(Thread.currentThread().getName() + " is over, num: " + data.num);
    }

}

class Data {

    int num = 0;

    public void add(int i) {
        this.num += i;
    }

}

結(jié)果:

thread-1 begin
thread-1 updated num: 10

(main waiting)

主線程while循環(huán)沒有結(jié)束猬膨,而是一直循環(huán)角撞,可見某個(gè)線程對(duì)于共享變量的修改對(duì)于其他線程是不可見的,線程讀取到的數(shù)據(jù)副本不會(huì)因其他線程修改而改變

現(xiàn)在我們把num變量添加volatile關(guān)鍵字

public class VolatileDemo {

    public static void main(String[] args) {

        visibility();

    }

    private static void visibility() {

        VolatileData data = new VolatileData();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " begin");

            // 等待3秒后更新data.num
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.add(10);
            System.out.println(Thread.currentThread().getName() + " updated num: " + data.num);
        }, "thread-1").start();

        // 主線程檢測(cè)當(dāng)data.num不為0時(shí)勃痴,結(jié)束循環(huán)谒所,否則一直等待
        while (data.num == 0) {

        }

        System.out.println(Thread.currentThread().getName() + " is over, num: " + data.num);
    }

}

class VolatileData {

    volatile int num = 0;

    public void add(int i) {
        this.num += i;
    }

}

結(jié)果:

thread-1 begin
thread-1 updated num: 10
main is over, num: 10

主線程感知到了其他線程對(duì)于data.num的修改,跳出循環(huán)執(zhí)行后面的語(yǔ)句沛申,并且主線程中data.num的值與thread-1線程中修改后的值一致劣领,所以說volatile關(guān)鍵字保證了線程間共享變量的可見性

不保證原子性

原子性表示操作的完整性,當(dāng)某個(gè)線程正在對(duì)某數(shù)據(jù)進(jìn)行操作的過程中铁材,操作過程不可分割尖淘,只能操作成功或者操作失敗。

例:

public class VolatileDemo {

    public static void main(String[] args) {

        nonAtomic();

    }

    private static void nonAtomic() {

        VolatileData data = new VolatileData();

        // 通過20個(gè)線程著觉,每個(gè)線程執(zhí)行1000次自增操作村生,共20000次自增操作
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.increase();
                }
            }, "thread-" + i).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " final num: " + data.num);

    }

}

class VolatileData {

    volatile int num = 0;

    public void add(int i) {
        this.num += i;
    }

    public void increase() {
        num++;
    }

}

結(jié)果:

main final num: 19450

如果volatile具有原子性,那么共執(zhí)行20000次的自增的結(jié)果應(yīng)該為20000饼丘,所以可見volatile關(guān)鍵字不保證對(duì)變量操作的原子性

我們通過javap -c反編譯字節(jié)碼文件的到increase()方法指令如下:

public void increase();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field num:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field num:I
      10: return

我們可以看到num++操作被轉(zhuǎn)化為了三個(gè)指令:

  1. getfield:獲取原始值
  2. iadd:執(zhí)行加1操作
  3. putfield:將修改后的值寫回

假設(shè)有兩個(gè)線程同時(shí)獲取到了原始值趁桃,線程1被掛起,線程2執(zhí)行自增并寫回,然后線程1執(zhí)行自增并寫回镇辉,由于兩個(gè)線程獲取到的原始值相同屡穗,所以兩個(gè)線程寫回的值也相同贴捡,這就導(dǎo)致了兩個(gè)線程的寫覆蓋忽肛,也就說明了操作不具有原子性

那么如何解決操作原子性問題呢?

  1. 對(duì)于volatile關(guān)鍵字修飾的變量操作添加synchronized關(guān)鍵字
  2. 使用原子變量AtomicInteger

例:

public class VolatileDemo {

    public static void main(String[] args) {

        nonAtomic();

    }

    private static void nonAtomic() {

        VolatileData data = new VolatileData();

        // 通過20個(gè)線程烂斋,每個(gè)線程執(zhí)行1000次自增操作屹逛,共20000次自增操作
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.increase();
                    data.atomicIncrease();
                }
            }, "thread-" + i).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " final num: " + data.num);
        System.out.println(Thread.currentThread().getName() + " final atomic num: " + data.atomicNum);

    }

}

class VolatileData {

    volatile int num = 0;

    AtomicInteger atomicNum = new AtomicInteger();

    public void add(int i) {
        this.num += i;
    }

    public void increase() {
        this.num++;
    }

    public void atomicIncrease() {
        this.atomicNum.getAndIncrement();
    }

}

結(jié)果:

main final num: 19801
main final atomic num: 20000

可見多個(gè)線程對(duì)于原子變量的操作具有原子性

禁止指令重排

計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能汛骂,編譯器和處理器常常對(duì)指令進(jìn)行重新排序罕模,一般會(huì)進(jìn)行如下三步:

image.png

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

處理器在進(jìn)行重排序時(shí)必須要考慮指令之間的數(shù)據(jù)依賴性

多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在帘瞭,兩個(gè)線程中使用的變量能否保證一致性是無法確定的淑掌,結(jié)果無法預(yù)測(cè)

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

首先了解一個(gè)概念蝶念,內(nèi)存屏障(Memory Barrier)也稱內(nèi)存柵欄抛腕,是一個(gè)CPU指令,它的作用有兩個(gè):

  1. 保證特定操作的執(zhí)行順序
  2. 保證某些變量的內(nèi)存可見性(volatile利用該特性實(shí)現(xiàn)可見性)

由于編譯器和處理器都能進(jìn)行執(zhí)行指令重排優(yōu)化媒殉,如果在指令間插入一條內(nèi)存屏障告訴編譯器和CPU担敌,不管什么指令都不能和這條內(nèi)存屏障指令重排序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化廷蓉。內(nèi)存屏障另外一個(gè)作用就是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù)全封,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本

對(duì)volatile變量進(jìn)行寫操作時(shí),會(huì)在寫操作后加一條store屏障指令桃犬,將工作內(nèi)存中的共享變量刷新回主內(nèi)存

image.png

對(duì)volatile變量進(jìn)行讀操作時(shí)刹悴,會(huì)在讀操作前加一條load屏障指令,從主內(nèi)存中讀取共享變量

image.png

保證線程安全性

  • 對(duì)于工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題攒暇,可以使用synchronizedvolatile關(guān)鍵字解決土匀,他們都可以是一個(gè)線程修改后的變量立即對(duì)其他線程可見
  • 對(duì)于指令重排導(dǎo)致的可見性問題和有序性問題,可以使用volatile關(guān)鍵字解決扯饶,因?yàn)?code>volatile關(guān)鍵字可以禁止重排序優(yōu)化
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恒削,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子尾序,更是在濱河造成了極大的恐慌钓丰,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件每币,死亡現(xiàn)場(chǎng)離奇詭異携丁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門梦鉴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來李茫,“玉大人,你說我怎么就攤上這事肥橙∑呛辏” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵存筏,是天一觀的道長(zhǎng)宠互。 經(jīng)常有香客問我,道長(zhǎng)椭坚,這世上最難降的妖魔是什么予跌? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮善茎,結(jié)果婚禮上券册,老公的妹妹穿的比我還像新娘。我一直安慰自己垂涯,他們只是感情好烁焙,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著集币,像睡著了一般考阱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鞠苟,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天乞榨,我揣著相機(jī)與錄音,去河邊找鬼当娱。 笑死吃既,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的跨细。 我是一名探鬼主播鹦倚,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼冀惭!你這毒婦竟也來了震叙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤散休,失蹤者是張志新(化名)和其女友劉穎媒楼,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戚丸,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡划址,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夺颤。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痢缎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出世澜,到底是詐尸還是另有隱情独旷,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布宜狐,位于F島的核電站势告,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏抚恒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一络拌、第九天 我趴在偏房一處隱蔽的房頂上張望俭驮。 院中可真熱鬧,春花似錦春贸、人聲如沸混萝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)逸嘀。三九已至,卻和暖如春允粤,著一層夾襖步出監(jiān)牢的瞬間崭倘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工类垫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留司光,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓悉患,卻偏偏與公主長(zhǎng)得像残家,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子售躁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348