前言
成都創(chuàng)新互聯(lián)公司堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:成都做網(wǎng)站、網(wǎng)站制作、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時(shí)代的雙流網(wǎng)站設(shè)計(jì)、移動(dòng)媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
熟悉 Java 并發(fā)編程的都知道,JMM(Java 內(nèi)存模型) 中的 happen-before(簡(jiǎn)稱 hb)規(guī)則,該規(guī)則定義了 Java 多線程操作的有序性和可見性,防止了編譯器重排序?qū)Τ绦蚪Y(jié)果的影響。
Java語言中有一個(gè)“先行發(fā)生”(happen—before)的規(guī)則,它是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果操作A先行發(fā)生于操作B,其意思就是說,在發(fā)生操作B之前,操作A產(chǎn)生的影響都能被操作B觀察到,“影響”包括修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等,它與時(shí)間上的先后發(fā)生基本沒有太大關(guān)系。這個(gè)原則特別重要,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)、線程是否安全的主要依據(jù)。
按照官方的說法:
當(dāng)一個(gè)變量被多個(gè)線程讀取并且至少被一個(gè)線程寫入時(shí),如果讀操作和寫操作沒有 HB 關(guān)系,則會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)問題。
要想保證操作 B 的線程看到操作 A 的結(jié)果(無論 A 和 B 是否在一個(gè)線程),那么在 A 和 B 之間必須滿足 HB 原則,如果沒有,將有可能導(dǎo)致重排序。
當(dāng)缺少 HB 關(guān)系時(shí),就可能出現(xiàn)重排序問題。
HB 有哪些規(guī)則?
這個(gè)大家都非常熟悉了應(yīng)該,大部分書籍和文章都會(huì)介紹,這里稍微回顧一下:
其中,傳遞規(guī)則我加粗了,這個(gè)規(guī)則至關(guān)重要。如何熟練的使用傳遞規(guī)則是實(shí)現(xiàn)同步的關(guān)鍵。
然后,再換個(gè)角度解釋 HB:當(dāng)一個(gè)操作 A HB 操作 B,那么,操作 A 對(duì)共享變量的操作結(jié)果對(duì)操作 B 都是可見的。
同時(shí),如果 操作 B HB 操作 C,那么,操作 A 對(duì)共享變量的操作結(jié)果對(duì)操作 B 都是可見的。
而實(shí)現(xiàn)可見性的原理則是 cache protocol 和 memory barrier。通過緩存一致性協(xié)議和內(nèi)存屏障實(shí)現(xiàn)可見性。
如何實(shí)現(xiàn)同步?
在 Doug Lea 著作 《Java Concurrency in Practice》中,有下面的描述:
書中提到:通過組合 hb 的一些規(guī)則,可以實(shí)現(xiàn)對(duì)某個(gè)未被鎖保護(hù)變量的可見性。
但由于這個(gè)技術(shù)對(duì)語句的順序很敏感,因此容易出錯(cuò)。
樓主接下來,將演示如何通過 volatile 規(guī)則和程序次序規(guī)則實(shí)現(xiàn)對(duì)一個(gè)變量同步。
來一個(gè)熟悉的例子:
class ThreadPrintDemo { static int num = 0; static volatile boolean flag = false; public static void main(String[] args) { Thread t1 = new Thread(() -> { for (; 100 > num; ) { if (!flag && (num == 0 || ++num % 2 == 0)) { System.out.println(num); flag = true; } } } ); Thread t2 = new Thread(() -> { for (; 100 > num; ) { if (flag && (++num % 2 != 0)) { System.out.println(num); flag = false; } } } ); t1.start(); t2.start(); } }
這段代碼的作用是兩個(gè)線程間隔打印出 0 - 100 的數(shù)字。
熟悉并發(fā)編程的同學(xué)肯定要說了,這個(gè) num 變量沒有使用 volatile,會(huì)有可見性問題,即:t1 線程更新了 num,t2 線程無法感知。
哈哈,樓主剛開始也是這么認(rèn)為的,但最近通過研究 HB 規(guī)則,我發(fā)現(xiàn),去掉 num 的 volatile 修飾也是可以的。
我們分析一下,樓主畫了一個(gè)圖:
我們分析這個(gè)圖:
注意:HB 規(guī)則保證上一個(gè)操作的結(jié)果對(duì)下一個(gè)操作都是可見的。
所以,上面的小程序中,線程 A 對(duì) num 的修改,線程 B 是完全感知的 —— 即使 num 沒有使用 volatile 修飾。
這樣,我們就借助 HB 原則實(shí)現(xiàn)了對(duì)一個(gè)變量的同步操作,也就是在多線程環(huán)境中,保證了并發(fā)修改共享變量的安全性。并且沒有對(duì)這個(gè)變量使用 Java 的原語:volatile 和 synchronized 和 CAS(假設(shè)算的話)。
這可能看起來不安全(實(shí)際上安全),也好像不太容易理解。因?yàn)檫@一切都是 HB 底層的 cache protocol 和 memory barrier 實(shí)現(xiàn)的。
其他規(guī)則實(shí)現(xiàn)同步
利用線程終結(jié)規(guī)則實(shí)現(xiàn):
static int a = 1; public static void main(String[] args) { Thread tb = new Thread(() -> { a = 2; }); Thread ta = new Thread(() -> { try { tb.join(); } catch (InterruptedException e) { //NO } System.out.println(a); }); ta.start(); tb.start(); }
利用線程 start 規(guī)則實(shí)現(xiàn):
static int a = 1; public static void main(String[] args) { Thread tb = new Thread(() -> { System.out.println(a); }); Thread ta = new Thread(() -> { tb.start(); a = 2; }); ta.start(); }
這兩個(gè)操作,也可以保證變量 a 的可見性。
確實(shí)有點(diǎn)顛覆之前的觀念。之前的觀念中,如果一個(gè)變量沒有被 volatile 修飾或 final 修飾,那么他在多線程下的讀寫肯定是不安全的 —— 因?yàn)闀?huì)有緩存,導(dǎo)致讀取到的不是最新的。
然而,通過借助 HB,我們可以實(shí)現(xiàn)。
總結(jié)
雖然本文標(biāo)題是通過 happen-before 實(shí)現(xiàn)對(duì)共享變量的同步操作,但主要目的還是更深刻的理解 happen-before,理解他的 happen-before 概念其實(shí)就是保證多線程環(huán)境中,上一個(gè)操作對(duì)下一個(gè)操作的有序性和操作結(jié)果的可見性。
同時(shí),通過靈活的使用傳遞性規(guī)則,再對(duì)規(guī)則進(jìn)行組合,就可以將兩個(gè)線程進(jìn)行同步 —— 實(shí)現(xiàn)指定的共享變量不使用原語也可以保證可見性。雖然這好像不是很易讀,但也是一種嘗試。
關(guān)于如何組合使用規(guī)則實(shí)現(xiàn)同步,Doug Lea 在 JUC 中給出了實(shí)踐。
例如老版本的 FutureTask 的內(nèi)部類 Sync(已消失),通過 tryReleaseShared 方法修改 volatile 變量,tryAcquireShared 讀取 volatile 變量,這是利用了 volatile 規(guī)則;
通過在 tryReleaseShared 之前設(shè)置非 volatile 的 result 變量,然后在 tryAcquireShared 之后讀取 result 變量,這是利用了程序次序規(guī)則。
從而保證 result 變量的可見性。和我們的第一個(gè)例子類似:利用程序次序規(guī)則和 volatile 規(guī)則實(shí)現(xiàn)普通變量可見性。
而 Doug Lea 自己也說了,這個(gè)“借助”技術(shù)非常容易出錯(cuò),要謹(jǐn)慎使用。但在某些情況下,這種“借助”是非常合理的。
實(shí)際上,BlockingQueue 也是“借助”了 happen-before 的規(guī)則。還記得 unlock 規(guī)則嗎?當(dāng) unlock 發(fā)生后,內(nèi)部元素一定是可見的。
而類庫中還有其他的操作也“借助”了 happen-before 原則:并發(fā)容器,CountDownLatch,Semaphore,F(xiàn)uture,Executor,CyclicBarrier,Exchanger 等。
總而言之,言而總之:
happen-before 原則是 JMM 的核心所在,只有滿足了 hb 原則才能保證有序性和可見性,否則編譯器將會(huì)對(duì)代碼重排序。hb 甚至將 lock 和 volatile 也定義了規(guī)則。
通過適當(dāng)?shù)膶?duì) hb 規(guī)則的組合,可以實(shí)現(xiàn)對(duì)普通共享變量的正確使用。
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)創(chuàng)新互聯(lián)的支持。
網(wǎng)站欄目:Java利用happen-before規(guī)則如何實(shí)現(xiàn)共享變量的同步操作詳解
URL網(wǎng)址:http://bm7419.com/article10/psdpgo.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制網(wǎng)站、手機(jī)網(wǎng)站建設(shè)、品牌網(wǎng)站制作、網(wǎng)頁設(shè)計(jì)公司、品牌網(wǎng)站建設(shè)、動(dòng)態(tài)網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)