熟悉 Java 併發編程的都知道,JMM(Java 內存模型) 中的 happens-before(簡稱 hb)規則,該規則定義了 Java 多線程操做的有序性和可見性,防止了編譯器重排序對程序結果的影響。按照官方的說法:java
當一個變量被多個線程讀取而且至少被一個線程寫入時,若是讀操做和寫操做沒有 HB 關係,則會產生數據競爭問題。 要想保證操做 B
的線程看到操做 A
的結果(不管 A
和 B
是否在一個線程),那麼在 A
和 B
之間必須知足 HB 原則,若是沒有,將有可能致使重排序。 當缺乏 HB 關係時,就可能出現重排序問題。編程
這個你們都很是熟悉了應該,大部分書籍和文章都會介紹,這裏稍微回顧一下:小程序
其中,傳遞規則我加粗了,這個規則相當重要。如何熟練的使用傳遞規則是實現同步的關鍵。緩存
而後,再換個角度解釋 HB:當一個操做 A HB 操做 B,那麼,操做 A 對共享變量的操做結果對操做 B 都是可見的。安全
同時,若是 操做 B HB 操做 C,那麼,操做 A 對共享變量的操做結果對操做 B 都是可見的。多線程
而實現可見性的原理則是 cache protocol 和 memory barrier。經過緩存一致性協議和內存屏障實現可見性。併發
在 Doug Lea 著做 《Java Concurrency in Practice》中,有下面的描述:app
書中提到:經過組合 hb 的一些規則,能夠實現對某個未被鎖保護變量的可見性。ui
但因爲這個技術對語句的順序很敏感,所以容易出錯。spa
樓主接下來,將演示如何經過 volatile 規則和程序次序規則實現對一個變量同步。
來一個熟悉的例子:
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(); } }
這段代碼的做用是兩個線程間隔打印出 0 – 100 的數字。
熟悉併發編程的同窗確定要說了,這個 num 變量沒有使用 volatile,會有可見性問題,即:t1 線程更新了 num,t2 線程沒法感知。
哈哈,樓主剛開始也是這麼認爲的,但最近經過研究 HB 規則,我發現,去掉 num 的 volatile 修飾也是能夠的。
咱們分析一下,樓主畫了一個圖:
咱們分析這個圖:
程序次序規則的
。也就是 1 HB 2.程序次序規則
,也就是 3 HB 4.傳遞性規則
,1 確定 HB 4. 因此,1 的修改對 4來講都是可見的。注意:HB 規則保證上一個操做的結果對下一個操做都是可見的。
因此,上面的小程序中,線程 A 對 num 的修改,線程 B 是徹底感知的 —— 即便 num 沒有使用 volatile 修飾。
這樣,咱們就藉助 HB 原則實現了對一個變量的同步操做,也就是在多線程環境中,保證了併發修改共享變量的安全性。而且沒有對這個變量使用 Java 的原語:volatile 和 synchronized 和 CAS(假設算的話)。
這可能看起來不安全(實際上安全),也好像不太容易理解。由於這一切都是 HB 底層的 cache protocol 和 memory barrier 實現的。
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(); }
2.利用線程 start 規則實現:
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(); }
這兩個操做,也能夠保證變量 a 的可見性。
確實有點顛覆以前的觀念。以前的觀念中,若是一個變量沒有被 volatile 修飾或 final 修飾,那麼他在多線程下的讀寫確定是不安全的 —— 由於會有緩存,致使讀取到的不是最新的。
然而,經過藉助 HB,咱們能夠實現。
雖然本文標題是經過 happens-before 實現對共享變量的同步操做,但主要目的仍是更深入的理解 happen-before,理解他的 happens-before 概念其實就是保證多線程環境中,上一個操做對下一個操做的有序性和操做結果的可見性。
同時,經過靈活的使用傳遞性規則,再對規則進行組合,就能夠將兩個線程進行同步 —— 實現指定的共享變量不使用原語也能夠保證可見性。雖然這好像不是很易讀,但也是一種嘗試。
關於如何組合使用規則實現同步,Doug Lea 在 JUC 中給出了實踐。
例如老版本的 FutureTask 的內部類 Sync(已消失),經過 tryReleaseShared 方法修改 volatile 變量,tryAcquireShared 讀取 volatile 變量,這是利用了 volatile 規則;
經過在 tryReleaseShared 以前設置非 volatile 的 result 變量,而後在 tryAcquireShared 以後讀取 result 變量,這是利用了程序次序規則。
從而保證 result 變量的可見性。和咱們的第一個例子相似:利用程序次序規則和 volatile 規則實現普通變量可見性。
而 Doug Lea 本身也說了,這個「藉助」技術很是容易出錯,要謹慎使用。但在某些狀況下,這種「藉助」是很是合理的。
實際上,BlockingQueue 也是「藉助」了 happens-before 的規則。還記得 unlock 規則嗎?當 unlock 發生後,內部元素必定是可見的。
而類庫中還有其餘的操做也「藉助」了 happens-before 原則:併發容器,CountDownLatch,Semaphore,Future,Executor,CyclicBarrier,Exchanger 等。
總而言之,言而總之:
happens-before 原則是 JMM 的核心所在,只有知足了 hb 原則才能保證有序性和可見性,不然編譯器將會對代碼重排序。hb 甚至將 lock 和 volatile 也定義了規則。
經過適當的對 hb 規則的組合,能夠實現對普通共享變量的正確使用。