日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入理解volatile

發(fā)布時(shí)間:2025/4/5 编程问答 17 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入理解volatile 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
在理解volatile之前,我們先來看下CPU的工作模式:



處理器這種工作產(chǎn)生的問題:1、所有的變量在處理器運(yùn)算期間都是變量對應(yīng)值的一個(gè)副本,其它處理器無法感知其對變量的操作。2、處理器為了高效利用寄存器而對指令的重排在多線程下將會(huì)產(chǎn)生無法預(yù)測的結(jié)果。3、不同的處理器針對同一套編碼所產(chǎn)生的指令會(huì)有不同的運(yùn)行策略。
為了解決上述三個(gè)問題JVM為了保證每個(gè)平臺(tái)代碼運(yùn)行結(jié)果的一致性提出了JMM(JAVA內(nèi)存模型),目的是為了讓Java程序在各種平臺(tái)下都能達(dá)到一致性的結(jié)果。
JMM規(guī)范:Happen-Before原則:1、程序順序原則:一個(gè)線程內(nèi)保證語意的串行化2、volatile規(guī)則:volatile變量的寫先發(fā)生于讀,這保證了volatile變量的可見性3、鎖規(guī)則:解鎖必然發(fā)生于加鎖前4、傳遞性:A先于B,B先于C,A一定先于C5、線程的start()方法先于它的每一個(gè)動(dòng)作6、線程的所有動(dòng)作,先于線程的終結(jié)7、線程的中斷先于被中斷的代碼8、對象的構(gòu)造函數(shù)執(zhí)行、結(jié)束先于finalize()方法
針對volatile的優(yōu)化:volatile能保證修改對其它線程可見。即修改了共享變量后肯定會(huì)刷回主內(nèi)存,通知其它線程,但是為了使處理器的內(nèi)部單元高效工作,處理器會(huì)對輸入的代碼進(jìn)行亂序即指令重排。對于volatile如果不做針對性的處理,那顯然volatile的可見性并不會(huì)有什么意義。并不能保證結(jié)果的確定性。針對volatileJVM做了大量的工作:關(guān)于工作內(nèi)存(針對硬件就是高速緩存)JMM定義了8種操作來完成:
  • lock(加鎖): 作用于主內(nèi)存,把一個(gè)變量標(biāo)記為線程獨(dú)占。
  • unlock(解鎖):作用于主內(nèi)存,把一個(gè)已鎖定的變量釋放出來。
  • read(讀取):作用于主內(nèi)存,將一個(gè)變量從主內(nèi)從中傳輸?shù)焦ぷ鲀?nèi)存中,以便隨后的load。
  • load(載入):作用于工作內(nèi)存,把read操作得到的變量放在工作內(nèi)存的變量副本中。
  • use(使用):作用于工作內(nèi)存,把工作內(nèi)存中的一個(gè)變量傳遞給執(zhí)行引擎。
  • assign(賦值):作用于工作內(nèi)存,把一個(gè)執(zhí)行引擎接受的值賦值給工作內(nèi)存的變量。
  • store(存儲(chǔ)):作用于工作內(nèi)存,把工作內(nèi)存中的一個(gè)變量的值傳輸?shù)街鲀?nèi)存,以便后續(xù)的write操作。
  • write(寫入):作用于主內(nèi)存,把store操作從工作內(nèi)存得到的值放回主內(nèi)存中。
8中操作有如下關(guān)系:
  • 不允許load和read,store和write單獨(dú)出現(xiàn)。
  • 不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變,必須同步回主內(nèi)存。
  • 不與許一個(gè)線程無原因的(沒有assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存。
  • 一個(gè)新的變量只能在主內(nèi)存中誕生。
  • 一個(gè)變量只能同時(shí)有一個(gè)線程進(jìn)行加鎖。lock可以被同一個(gè)線程加鎖多次,但是必須解鎖相同次數(shù)。這個(gè)變量才會(huì)被解鎖。
  • 對一個(gè)變量執(zhí)行l(wèi)ock操作。將會(huì)先清空該線程的工作內(nèi)存中的該變量的值。在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作。
  • 一個(gè)變量被lock,不允許其它線程執(zhí)行unlock。也不允許執(zhí)行unlock被別的線程lock的變量。即一個(gè)線程自己lock的只有自己能unlock.
  • 一個(gè)變量unlock之前,工作內(nèi)存中的數(shù)據(jù)必須同步回主內(nèi)存。
這八種操作和其使用規(guī)則,決定了變量在工作內(nèi)存和主內(nèi)存之間的同步策略。
針對于volatile變量又有額外如下定義:
  • volatile變量在use時(shí),必須執(zhí)行l(wèi)oad操作。即每次使用volatile變量必須先從主內(nèi)存中刷新最新值。
  • volatile變量在assign時(shí),必須執(zhí)行write操作。即每次對volatile進(jìn)行賦值操作必須立馬同步回主內(nèi)存。
  • 針對volatile和普通變量,或者volatile變量和volatile變量一起使用時(shí)。
    JVM在編譯期間也會(huì)針對volatile的重排加以干涉,干涉規(guī)則如下:


  • 如果第二個(gè)操作時(shí)volatile寫操作,不管第一操作是什么操作,都不能重排。
  • 如果第一個(gè)操作時(shí)volatile讀操作,不管第二個(gè)操作時(shí)什么操作,都不能重排。
  • volatile寫和volatile讀不能重排。

  • 為了實(shí)現(xiàn)這個(gè)語意,JVM在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障(memory barrier)來禁止特定類型的處理器指令重排,對于編譯器來說對所有的CPU來插入屏障數(shù)最小的方案幾乎不可能,下面是基于保守策略的JMM內(nèi)存屏障插入策略:
  • 在每個(gè)volatile寫操作前面插入StoreStore屏障
  • 在每個(gè)volatile寫操作后插入StoreLoad屏障
  • 在每個(gè)volatile讀后面插入一個(gè)LoadLoad屏障
  • 在每個(gè)volatile讀后面插入一個(gè)LoadStore屏障

  • 這里要說下內(nèi)存屏障是是什么東西:硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障,內(nèi)存屏障的作用有兩個(gè):
    • 阻止屏障兩側(cè)的的指令重排
    • 強(qiáng)制把高速緩存中的數(shù)據(jù)更新或者寫入到主存中。Load Barrier負(fù)責(zé)更新高速緩存, Store Barrier負(fù)責(zé)將高速緩沖區(qū)的內(nèi)容寫回主存

    LoadLoad,StoreStore,LoadStore,StoreLoad實(shí)際上是Java對上面兩種屏障的組合,來完成一系列的屏障和數(shù)據(jù)同步功能:
    • LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
    • StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
    • LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
    • StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數(shù)處理器的實(shí)現(xiàn)中,這個(gè)屏障是個(gè)萬能屏障,兼具其它三種內(nèi)存屏障的功能。



    • StoreStore屏障可以保證在volatile寫之前,所有的普通寫操作已經(jīng)對所有處理器可見,StoreStore屏障保障了在volatile寫之前所有的普通寫操作已經(jīng)刷新到主存。
    • StoreLoad屏障避免volatile寫與下面有可能出現(xiàn)的volatile讀/寫操作重排。因?yàn)榫幾g器無法準(zhǔn)確判斷一個(gè)volatile寫后面是否需要插入一個(gè)StoreLoad屏障(寫之后直接就return了,這時(shí)其實(shí)沒必要加StoreLoad屏障),為了能實(shí)現(xiàn)volatile的正確內(nèi)存語意,JVM采取了保守的策略。在每個(gè)volatile寫之后或每個(gè)volatile讀之前加上一個(gè)StoreLoad屏障,而大多數(shù)場景是一個(gè)線程寫volatile變量多個(gè)線程去讀volatile變量,同一時(shí)刻讀的線程數(shù)量其實(shí)遠(yuǎn)大于寫的線程數(shù)量。選擇在volatile寫后面加入StoreLoad屏障將大大提升執(zhí)行效率(上面已經(jīng)說了StoreLoad屏障的開銷是很大的)。


    • LoadLoad屏障保證了volatile讀不會(huì)與下面的普通讀發(fā)生重排
    • LoadStore屏障保證了volatile讀不回與下面的普通寫發(fā)生重排。

    即使JMM對volatile做了這么多的工作,它也僅僅只保證了volatile變量在原子性操作下多個(gè)線程之間的正確同步,對非原子操作,使用volatile仍然會(huì)發(fā)生無法預(yù)知的結(jié)果。比如對i++操作,在多線程情況下結(jié)果依然是不定:例子:



    我們來使用 javap -c 來看下這個(gè)文件的編譯指令:


    increase方法的編譯指令我們可以看出 ++ 操作經(jīng)歷了4步:1、getstatic #10 獲取靜態(tài)變量num壓入棧頂 此時(shí)volatile保證值是對的。2、iconst_1 int型常量1入棧3、iadd 棧頂兩個(gè)int值相加,結(jié)果放入棧頂。4、putstatic #10 把棧頂?shù)闹地?fù)值給指定域。問題就出在2、3兩步,在做這兩步操作時(shí),volatile變量有可能已經(jīng)被其它線程修改。
    根據(jù)volatile的內(nèi)存語意我們可以總結(jié)出兩條安全使用volatile的方式:
    • 運(yùn)算結(jié)果不依賴于volatile變量的當(dāng)前值,或者能保證只有單一線程能修改變量的值
    • 變量不需要與其它的狀態(tài)變量共同參與不變性。



    轉(zhuǎn)載于:https://juejin.im/post/5bb4a26fe51d450e7b174c16

    總結(jié)

    以上是生活随笔為你收集整理的深入理解volatile的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。