阿里面試題Java 并發(fā)編程之 happens-before 規(guī)則丨【奔跑吧!JAVA】

      網(wǎng)友投稿 945 2022-05-29

      我是陳皮,一個(gè)在互聯(lián)網(wǎng) Coding 的 ITer,微信搜索「陳皮的JavaLib」第一時(shí)間閱讀最新文章,回復(fù)【資料】,即可獲得我精心整理的技術(shù)資料,電子書籍,一線大廠面試資料和優(yōu)秀簡(jiǎn)歷模板。

      引言

      happens-before 字面意思就是先行發(fā)生,你可以理解為 A happens before B,就是 A 發(fā)生在 B 之前。

      happens-before(HB) 是在 JMM 中的一個(gè)很重要的規(guī)則,即一個(gè)操作的結(jié)果對(duì)于另一個(gè)操作是可見的,用來指定兩個(gè)操作之間的執(zhí)行順序。

      那為什么要有這個(gè)規(guī)則呢?其一是為了解決多線程的共享數(shù)據(jù)的可見性問題;其二是為了解決一些指令重排序問題,JMM 對(duì)編譯器和處理器指令重排序的約束原則。唯有如此,才能保證我們寫的程序按我們預(yù)想的方式執(zhí)行,得到想要的結(jié)果。

      假設(shè)程序有兩個(gè)操作 A 和 B,B 操作需要 A 操作的結(jié)果。這兩個(gè)操作可以在在同一線程中完成,或者在兩個(gè)不同的線程中完成,happens-before 能向我們保證 A 操作的結(jié)果對(duì) B 操作是可見的。A 和 B 之間存在 happens-before 關(guān)系。

      JMM

      JMM 即 Java 內(nèi)存模型,它是對(duì)共享內(nèi)存的并發(fā)模型。我們知道,Java 中的共享變量是存儲(chǔ)在主內(nèi)存中的,而線程有自己的工作內(nèi)存,如果一個(gè)線程要操作一個(gè)共享變量,它會(huì)將共享變量賦值一份到自己的工作內(nèi)存,進(jìn)行操作后,再將最新的變量值回寫到主內(nèi)存中。

      假設(shè)有線程 a 對(duì)共享變量 x 讀取進(jìn)行更新后及時(shí)回寫到主內(nèi)存,然后線程 b 再讀取共享變量 x 的值進(jìn)行操作,這是正常沒問題的。但是如果線程 a 對(duì)共享變量 x 更新后沒有及時(shí)回寫到主內(nèi)存,這時(shí)線程 b 讀取到共享變量 x 的值進(jìn)行操作,這就出現(xiàn)臟讀的現(xiàn)象。

      要處理以上并發(fā)臟讀的問題也簡(jiǎn)單,可以使用同步機(jī)制控制多線程之間操作的順序,例如使用 synchronzied 關(guān)鍵字;或者使用 volatitle 關(guān)鍵字強(qiáng)制將線程更新后的最新值回寫到主內(nèi)存,以便其他線程可見。這都是 JMM 中 HB 的體現(xiàn)。

      指令重排序

      我們知道,JVM 會(huì)對(duì)我們寫的代碼進(jìn)行優(yōu)化,其中一個(gè)優(yōu)化點(diǎn)就是編譯器和處理器對(duì)指令進(jìn)行重排序。雖然這些優(yōu)化能提高程序性能,但是有可能會(huì)出現(xiàn)優(yōu)化后執(zhí)行的結(jié)果不是我們預(yù)想的結(jié)果,所以需要 happens-before 規(guī)則來禁止一些編譯優(yōu)化的場(chǎng)景,保證并發(fā)編程的正確性。

      JMM 并不是全部禁止指令重排序,對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM 允許編譯器和處理器這樣做。而對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM 會(huì)禁止這種重排序。

      以我們最熟悉的 double-check 懶漢式單例模式為例,網(wǎng)上會(huì)告知如下程序是沒問題的。

      package com.chenpi; /** * @Description * @Author Mr.nobody * @Date 2021/6/22 * @Version 1.0 */ public class Singleton { private static Singleton singleton = null; private Singleton() {} public static Singleton getInstance() { if (null == singleton) { synchronized (Singleton.class) { if (null == singleton) { singleton = new Singleton(); } } } return singleton; } }

      如果分析底層的話,以下一行代碼并不是原子操作。在 JVM 指令執(zhí)行來看,一般會(huì)有如下步驟:

      分配內(nèi)存給對(duì)象

      初始化對(duì)象,即生成實(shí)例

      阿里面試題:Java 并發(fā)編程之 happens-before 規(guī)則丨【奔跑吧!JAVA】

      將分配的內(nèi)存地址賦值給對(duì)象,此時(shí)對(duì)象不為null

      如果按上面的步驟執(zhí)行的話,并發(fā)情況不會(huì)有問題。但是因?yàn)橛捎?JVM 編譯優(yōu)化的影響,有可能導(dǎo)致2和3步驟顛倒位置,即發(fā)生指令重排序。如果執(zhí)行了第3步驟而還沒執(zhí)行第2步驟,在并發(fā)情況下,如果有另一個(gè)線程判斷此時(shí)變量不為 null,則返回沒有初始化的對(duì)象進(jìn)行使用,就有可能會(huì)出現(xiàn)報(bào)錯(cuò)。

      singleton = new Singleton();

      解決上述的問題也簡(jiǎn)單,用 volatitle 關(guān)鍵字修飾 singleton 變量即可,這也是 happens-before 規(guī)則之一。

      private static volatile Singleton singleton = null;

      為何要有 happens-before

      其實(shí) happens-before 更像一個(gè)發(fā)揮中間層的作用。向我們程序員保證一些操作之間的執(zhí)行順序,例如 A happens-before B,就保證 A 一定先行發(fā)生于 B。它讓我們能以簡(jiǎn)單易懂的方式去寫代碼,而不用去學(xué)習(xí)底層比較復(fù)雜的例如內(nèi)存模型,指令重排序等知識(shí)。其二是對(duì)編譯器和處理器做一些約束,它們可以做任何代碼優(yōu)化,但是得保證優(yōu)化后不能改變?cè)谐绦虻膱?zhí)行結(jié)果,這里主要指單線程程序和正確同步的多線程程序,不然禁止它們優(yōu)化。

      簡(jiǎn)而言之,就是 happens-before 向我們保證了在多線程環(huán)境中,上一個(gè)操作對(duì)下一個(gè)操作的有序性和操作結(jié)果的可見性。

      其實(shí) Java JUC 類庫中許多操作都使用到了 happens-before 規(guī)則,例如并發(fā)容器,CountDownLatch,Semaphore,F(xiàn)uture,Executor等。

      何為理解在 happens-before 規(guī)則的情況下,也允許編譯器和處理器進(jìn)行優(yōu)化呢?例如程序有一個(gè)加鎖的操作,但是編譯器分析發(fā)現(xiàn)這個(gè)鎖只能被單線程訪問,那其實(shí)就沒必要加鎖操作了,就可以優(yōu)化消除這個(gè)鎖。這樣能提高程序的執(zhí)行效率,還不會(huì)改變?cè)械膱?zhí)行結(jié)果。

      happens-before 規(guī)則

      程序次序規(guī)則:同一個(gè)線程內(nèi)的一段代碼的執(zhí)行順序是有序的,即前面的操作 happens-before 后面的操作。但還是有可能發(fā)生指令重排序,不過重排序后的結(jié)果還是跟順序執(zhí)行的結(jié)果一致。

      管程鎖定規(guī)則:對(duì)一個(gè)鎖的解鎖操作 happens-before 后續(xù)對(duì)這個(gè)鎖的加鎖操作。即后續(xù)的加鎖操作能夠感知到前面解鎖的變化,synchronized 就是管程的實(shí)現(xiàn)。

      volatile 變量規(guī)則:對(duì) valatile 修飾的變量的更新操作 happens-before 后續(xù)對(duì)此變量的任意操作。可以了解下內(nèi)存屏障和緩存一致性協(xié)議。

      傳遞性規(guī)則:A happens-before B,B happens-before C,則 A happens-before C。學(xué)過離散數(shù)學(xué)的都知道偏序關(guān)系,偏序關(guān)系具有傳遞性。

      線程啟動(dòng)規(guī)則:在主線程啟動(dòng)子線程,那么主線程啟動(dòng)子線程之前的操作對(duì)于子線程是可見的。即 start() happens-before 子線程中的操作。

      線程終止規(guī)則:在主線程執(zhí)行過程中,子線程終止,那么子線程終止之前的操作在主線程中是可見。例如在主線程中執(zhí)行子線程的 join 方法等待子線程完成,當(dāng)子線程執(zhí)行完畢后,主線程可以看到子線程的所有操作。

      線程中斷規(guī)則:對(duì)線程 interrupt 方法的調(diào)用 happens-before 被中斷線程代碼檢測(cè)到中斷事件。

      對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的構(gòu)造函數(shù)執(zhí)行的結(jié)束 happens-before 它的 finalize()方法。

      【奔跑吧!JAVA】有獎(jiǎng)?wù)魑幕馃徇M(jìn)行中:https://bbs.huaweicloud.com/blogs/265241

      Java JVM 多線程

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:主題模型LDA的實(shí)現(xiàn)
      下一篇:Python 前端開發(fā)之CSS標(biāo)準(zhǔn)文檔流及元素
      相關(guān)文章
      国产精品成人亚洲| 国产亚洲高清在线精品不卡| 超清首页国产亚洲丝袜| 国产综合成人亚洲区| 色综合久久精品亚洲国产| 亚洲av成人无码网站…| 亚洲成在人线aⅴ免费毛片| 亚洲精品乱码久久久久蜜桃| 亚洲精品无码av片| 日本亚洲欧美色视频在线播放| 亚洲jizzjizz少妇| 国产成人+综合亚洲+天堂| 久久精品国产亚洲av品善| 国产精品亚洲一区二区无码| 国产偷国产偷亚洲高清在线| 无码国产亚洲日韩国精品视频一区二区三区 | 亚洲无线一二三四区手机| 亚洲精品人成无码中文毛片| 亚洲人妻av伦理| 亚洲无人区一区二区三区| 国产亚洲精品成人AA片新蒲金| 在线观看亚洲精品福利片| 亚洲精品国产品国语在线| 亚洲av之男人的天堂网站| 亚洲国产精品久久久久网站| 亚洲狠狠久久综合一区77777| 亚洲视频在线一区二区三区| 亚洲剧情在线观看| 亚洲中文字幕无码中文| 蜜臀亚洲AV无码精品国产午夜.| www.亚洲精品.com| 浮力影院亚洲国产第一页| 亚洲高清专区日韩精品| 4480yy私人影院亚洲| www.亚洲成在线| 亚洲av成人无码网站…| 日日噜噜噜噜夜夜爽亚洲精品 | 亚洲国产成人一区二区三区| 久久夜色精品国产噜噜噜亚洲AV| 亚洲精品国产福利在线观看| 亚洲人精品亚洲人成在线|