volatile深入解析

談?wù)勀銓?volatile 的理解?

你知道 volatile 底層的實(shí)現(xiàn)機(jī)制嗎滑燃?

volatile 變量和 atomic 變量有什么不同役听?

volatile 的使用場景,你能舉兩個(gè)例子嗎表窘?

文章收錄在 GitHub JavaKeeper 典予,包含 N 線互聯(lián)網(wǎng)開發(fā)必備技能兵器譜

之前算是比較詳細(xì)的介紹了 Java 內(nèi)存模型——JMM, JMM是圍繞著并發(fā)過程中如何處理可見性乐严、原子性和有序性這 3 個(gè) 特征建立起來的瘤袖,而 volatile 可以保證其中的兩個(gè)特性,下面具體探討下這個(gè)面試必問的關(guān)鍵字昂验。

img

1. 概念

volatile 是 Java 中的關(guān)鍵字捂敌,是一個(gè)變量修飾符,用來修飾會(huì)被不同線程訪問和修改的變量既琴。


2. Java 內(nèi)存模型 3 個(gè)特性

2.1 可見性

可見性是一種復(fù)雜的屬性占婉,因?yàn)榭梢娦灾械腻e(cuò)誤總是會(huì)違背我們的直覺。通常甫恩,我們無法確保執(zhí)行讀操作的線程能適時(shí)地看到其他線程寫入的值逆济,有時(shí)甚至是根本不可能的事情。為了確保多個(gè)線程之間對內(nèi)存寫入操作的可見性,必須使用同步機(jī)制奖慌。

可見性抛虫,是指線程之間的可見性,一個(gè)線程修改的狀態(tài)對另一個(gè)線程是可見的升薯。也就是一個(gè)線程修改的結(jié)果莱褒。另一個(gè)線程馬上就能看到。

在 Java 中 volatile涎劈、synchronized 和 final 都可以實(shí)現(xiàn)可見性广凸。

2.2 原子性

原子性指的是某個(gè)線程正在執(zhí)行某個(gè)操作時(shí),中間不可以被加塞或分割蛛枚,要么整體成功谅海,要么整體失敗。比如 a=0蹦浦;(a非long和double類型) 這個(gè)操作是不可分割的扭吁,那么我們說這個(gè)操作是原子操作。再比如:a++盲镶; 這個(gè)操作實(shí)際是a = a + 1侥袜;是可分割的,所以他不是一個(gè)原子操作溉贿。非原子操作都會(huì)存在線程安全問題枫吧,需要我們使用同步技術(shù)(sychronized)來讓它變成一個(gè)原子操作。一個(gè)操作是原子操作宇色,那么我們稱它具有原子性九杂。Java的 concurrent 包下提供了一些原子類,AtomicInteger宣蠕、AtomicLong例隆、AtomicReference等。

在 Java 中 synchronized 和在 lock抢蚀、unlock 中操作保證原子性镀层。

2.3 有序性

Java 語言提供了 volatile 和 synchronized 兩個(gè)關(guān)鍵字來保證線程之間操作的有序性,volatile 是因?yàn)槠浔旧戆敖怪噶钪嘏判颉钡恼Z義思币,synchronized 是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對其進(jìn)行 lock 操作”這條規(guī)則獲得的鹿响,此規(guī)則決定了持有同一個(gè)對象鎖的兩個(gè)同步塊只能串行執(zhí)行。


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

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排(保證有序性)

3.1 空說無憑谷饿,代碼驗(yàn)證

3.1.1 可見性驗(yàn)證

class MyData {
    int number = 0;
    public void add() {
        this.number = number + 1;
    }
}

   // 啟動(dòng)兩個(gè)線程惶我,一個(gè)work線程,一個(gè)main線程博投,work線程修改number值后绸贡,查看main線程的number
   private static void testVolatile() {
        MyData myData = new MyData();
     
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try {
                TimeUnit.SECONDS.sleep(2);
                myData.add();
                System.out.println(Thread.currentThread().getName()+"\t update number value :"+myData.number);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "workThread").start();

        //第2個(gè)線程,main線程
        while (myData.number == 0){
            //main線程還在找0
        }
        System.out.println(Thread.currentThread().getName()+"\t mission is over");      
        System.out.println(Thread.currentThread().getName()+"\t mission is over,main get number is:"+myData.number);
    }
}

運(yùn)行 testVolatile() 方法听怕,輸出如下捧挺,會(huì)發(fā)現(xiàn)在 main 線程死循環(huán),說明 main 線程的值一直是 0

workThread   execute
workThread   update number value :1

修改 volatile int number = 0,尿瞭,在 number 前加關(guān)鍵字 volatile,重新運(yùn)行闽烙,main 線程獲取結(jié)果為 1

workThread   execute
workThread   update number value :1
main     execute over,main get number is:1

3.1.2 不保證原子性驗(yàn)證

class MyData {
    volatile int number = 0;
    public void add() {
        this.number = number + 1;
    }
}

private static void testAtomic() throws InterruptedException {
  MyData myData = new MyData();

  for (int i = 0; i < 10; i++) {
    new Thread(() ->{
      for (int j = 0; j < 1000; j++) {
        myData.addPlusPlus();
      }
    },"addPlusThread:"+ i).start();
  }


  //等待上邊20個(gè)線程結(jié)束后(預(yù)計(jì)5秒肯定結(jié)束了)声搁,在main線程中獲取最后的number
  TimeUnit.SECONDS.sleep(5);
  while (Thread.activeCount() > 2){
    Thread.yield();
  }
  System.out.println("final value:"+myData.number);
}

運(yùn)行 testAtomic 發(fā)現(xiàn)最后的輸出值黑竞,并不一定是期望的值 10000,往往是比 10000 小的數(shù)值疏旨。

final value:9856

為什么會(huì)這樣呢很魂,因?yàn)?i++ 在轉(zhuǎn)化為字節(jié)碼指令的時(shí)候是4條指令

  • getfield 獲取原始值
  • iconst_1 將值入棧
  • iadd 進(jìn)行加 1 操作
  • putfieldiadd 后的操作寫回主內(nèi)存

這樣在運(yùn)行時(shí)候就會(huì)存在多線程競爭問題,可能會(huì)出現(xiàn)了丟失寫值的情況檐涝。

image

如何解決原子性問題呢遏匆?

synchronized 或者直接使用 Automic 原子類。

3.1.3 禁止指令重排驗(yàn)證

計(jì)算機(jī)在執(zhí)行程序時(shí)谁榜,為了提高性能幅聘,編譯器和處理器常常會(huì)對指令做重排,一般分為以下 3 種

img

處理器在進(jìn)行重排序時(shí)必須要考慮指令之間的數(shù)據(jù)依賴性窃植,我們叫做 as-if-serial 語義

單線程環(huán)境里確保程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致喊暖;但是多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在撕瞧,兩個(gè)線程中使用的變量能否保證一致性是無法確定的,結(jié)果無法預(yù)測狞尔。

我們往往用下面的代碼驗(yàn)證 volatile 禁止指令重排丛版,如果多線程環(huán)境下,`最后的輸出結(jié)果不一定是我們想象到的 2偏序,這時(shí)就要把兩個(gè)變量都設(shè)置為 volatile页畦。

public class ReSortSeqDemo {

    int a = 0;
    boolean flag = false;

    public void mehtod1(){
        a = 1;
        flag = true;
    }

    public void method2(){
        if(flag){
            a = a +1;
            System.out.println("reorder value: "+a);
        }
    }
}

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

還有一個(gè)我們最常見的多線程環(huán)境中 DCL(double-checked locking) 版本的單例模式中豫缨,就是使用了 volatile 禁止指令重排的特性。

public class Singleton {

    private static volatile Singleton instance;
  
    private Singleton(){}
    // DCL
    public static Singleton getInstance(){
        if(instance ==null){   //第一次檢查
            synchronized (Singleton.class){
                if(instance == null){   //第二次檢查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

因?yàn)橛兄噶钪嘏判虻拇嬖诙硕洌p端檢索機(jī)制也不一定是線程安全的好芭。

why ?

Because: instance = new Singleton(); 初始化對象的過程其實(shí)并不是一個(gè)原子的操作,它會(huì)分為三部分執(zhí)行冲呢,

  1. 給 instance 分配內(nèi)存
  2. 調(diào)用 instance 的構(gòu)造函數(shù)來初始化對象
  3. 將 instance 對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)

步驟 2 和 3 不存在數(shù)據(jù)依賴關(guān)系舍败,如果虛擬機(jī)存在指令重排序優(yōu)化,則步驟 2和 3 的順序是無法確定的。如果A線程率先進(jìn)入同步代碼塊并先執(zhí)行了 3 而沒有執(zhí)行 2邻薯,此時(shí)因?yàn)?instance 已經(jīng)非 null裙戏。這時(shí)候線程 B 在第一次檢查的時(shí)候,會(huì)發(fā)現(xiàn) instance 已經(jīng)是 非null 了厕诡,就將其返回使用累榜,但是此時(shí) instance 實(shí)際上還未初始化,自然就會(huì)出錯(cuò)灵嫌。所以我們要限制實(shí)例對象的指令重排壹罚,用 volatile 修飾(JDK 5 之前使用了 volatile 的雙檢鎖是有問題的)。


4. 原理

volatile 可以保證線程可見性且提供了一定的有序性醒第,但是無法保證原子性渔嚷。在 JVM 底層是基于內(nèi)存屏障實(shí)現(xiàn)的。

  • 當(dāng)對非 volatile 變量進(jìn)行讀寫的時(shí)候稠曼,每個(gè)線程先從內(nèi)存拷貝變量到 CPU 緩存中形病。如果計(jì)算機(jī)有多個(gè)CPU,每個(gè)線程可能在不同的 CPU 上被處理霞幅,這意味著每個(gè)線程可以拷貝到不同的 CPU cache 中
  • 而聲明變量是 volatile 的漠吻,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步司恳,所以就不會(huì)有可見性問題
    • 對 volatile 變量進(jìn)行寫操作時(shí)途乃,會(huì)在寫操作后加一條 store 屏障指令,將工作內(nèi)存中的共享變量刷新回主內(nèi)存扔傅;
    • 對 volatile 變量進(jìn)行讀操作時(shí)耍共,會(huì)在寫操作后加一條 load 屏障指令,從主內(nèi)存中讀取共享變量猎塞;

通過 hsdis 工具獲取 JIT 編譯器生成的匯編指令來看看對 volatile 進(jìn)行寫操作CPU會(huì)做什么事情试读,還是用上邊的單例模式,可以看到

image

(PS:具體的匯編指令對我這個(gè) Javaer 太南了荠耽,但是 JVM 字節(jié)碼我們可以認(rèn)識(shí)钩骇,putstatic 的含義是給一個(gè)靜態(tài)變量設(shè)置值,那這里的 putstatic instance ,而且是第 17 行代碼铝量,更加確定是給 instance 賦值了倘屹。果然像各種資料里說的,找到了 lock add1 據(jù)說還得翻閱慢叨。這里可以看下這兩篇 http://www.reibang.com/p/6ab7c3db13c3 纽匙、 https://www.cnblogs.com/xrq730/p/7048693.html

有 volatile 修飾的共享變量進(jìn)行寫操作時(shí)會(huì)多出第二行匯編代碼,該句代碼的意思是對原值加零插爹,其中相加指令addl前有 lock 修飾哄辣。通過查IA-32架構(gòu)軟件開發(fā)者手冊可知请梢,lock前綴的指令在多核處理器下會(huì)引發(fā)兩件事情:

  • 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
  • 這個(gè)寫回內(nèi)存的操作會(huì)引起在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效

正是 lock 實(shí)現(xiàn)了 volatile 的「防止指令重排」「內(nèi)存可見」的特性


5. 使用場景

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全力穗,必須同時(shí)滿足下面兩個(gè)條件:

  • 對變量的寫操作不依賴于當(dāng)前值
  • 該變量沒有包含在具有其他變量的不變式中

其實(shí)就是在需要保證原子性的場景毅弧,不要使用 volatile。


5. volatile 性能

volatile 的讀性能消耗與普通變量幾乎相同当窗,但是寫操作稍慢够坐,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。

引用《正確使用 volaitle 變量》一文中的話:

很難做出準(zhǔn)確崖面、全面的評(píng)價(jià)元咙,例如 “X 總是比 Y 快”,尤其是對 JVM 內(nèi)在的操作而言巫员。(例如庶香,某些情況下 JVM 也許能夠完全刪除鎖機(jī)制,這使得我們難以抽象地比較 volatilesynchronized 的開銷简识。)就是說赶掖,在目前大多數(shù)的處理器架構(gòu)上,volatile 讀操作開銷非常低 —— 幾乎和非 volatile 讀操作一樣七扰。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多奢赂,因?yàn)橐WC可見性需要實(shí)現(xiàn)內(nèi)存界定(Memory Fence),即便如此颈走,volatile 的總開銷仍然要比鎖獲取低膳灶。

volatile 操作不會(huì)像鎖一樣造成阻塞,因此立由,在能夠安全使用 volatile 的情況下轧钓,volatile 可以提供一些優(yōu)于鎖的可伸縮特性。如果讀操作的次數(shù)要遠(yuǎn)遠(yuǎn)超過寫操作锐膜,與鎖相比聋迎,volatile 變量通常能夠減少同步的性能開銷。

參考

《深入理解Java虛擬機(jī)》
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
https://juejin.im/post/5dbfa0aa51882538ce1a4ebc
《正確使用 Volatile 變量》https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

圖怪獸_b2195efd95f95e83c90c74142a4b2001_47863.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末枣耀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子庭再,更是在濱河造成了極大的恐慌捞奕,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拄轻,死亡現(xiàn)場離奇詭異颅围,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)恨搓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門院促,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筏养,“玉大人,你說我怎么就攤上這事常拓〗ト埽” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵弄抬,是天一觀的道長茎辐。 經(jīng)常有香客問我,道長掂恕,這世上最難降的妖魔是什么拖陆? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮懊亡,結(jié)果婚禮上依啰,老公的妹妹穿的比我還像新娘。我一直安慰自己店枣,他們只是感情好速警,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著艰争,像睡著了一般坏瞄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上甩卓,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天鸠匀,我揣著相機(jī)與錄音,去河邊找鬼逾柿。 笑死缀棍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的机错。 我是一名探鬼主播爬范,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弱匪!你這毒婦竟也來了青瀑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對情侶失蹤萧诫,失蹤者是張志新(化名)和其女友劉穎斥难,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帘饶,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哑诊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了及刻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片镀裤。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡竞阐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出暑劝,到底是詐尸還是另有隱情骆莹,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布铃岔,位于F島的核電站汪疮,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏毁习。R本人自食惡果不足惜智嚷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望纺且。 院中可真熱鬧盏道,春花似錦、人聲如沸载碌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嫁艇。三九已至朗伶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間步咪,已是汗流浹背论皆。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工雕蔽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留糜芳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓匾荆,卻偏偏與公主長得像悯周,于是被迫代替她去往敵國和親粒督。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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