Java的面向?qū)ο缶幊?/a>">Java的面向?qū)ο缶幊?/a>
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í)例
將分配的內(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)容。