溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java如何使用happens-before規(guī)則實(shí)現(xiàn)共享變量的同步操作

發(fā)布時(shí)間:2021-06-12 17:03:29 來(lái)源:億速云 閱讀:234 作者:小新 欄目:大數(shù)據(jù)

小編給大家分享一下Java如何使用happens-before規(guī)則實(shí)現(xiàn)共享變量的同步操作,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

前言

熟悉 Java 并發(fā)編程的都知道,JMM(Java 內(nèi)存模型) 中的 happens-before(簡(jiǎn)稱 hb)規(guī)則,該規(guī)則定義了 Java 多線程操作的有序性和可見(jiàn)性,防止了編譯器重排序?qū)Τ绦蚪Y(jié)果的影響。按照官方的說(shuō)法:

當(dāng)一個(gè)變量被多個(gè)線程讀取并且至少被一個(gè)線程寫(xiě)入時(shí),如果讀操作和寫(xiě)操作沒(méi)有 HB 關(guān)系,則會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題。 要想保證操作 B 的線程看到操作 A 的結(jié)果(無(wú)論 A 和 B 是否在一個(gè)線程),那么在 A 和 B 之間必須滿足 HB 原則,如果沒(méi)有,將有可能導(dǎo)致重排序。 當(dāng)缺少 HB 關(guān)系時(shí),就可能出現(xiàn)重排序問(wèn)題。

HB 有哪些規(guī)則?

這個(gè)大家都非常熟悉了應(yīng)該,大部分書(shū)籍和文章都會(huì)介紹,這里稍微回顧一下:

  1. 程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作;

  2. 鎖定規(guī)則:在監(jiān)視器鎖上的解鎖操作必須在同一個(gè)監(jiān)視器上的加鎖操作之前執(zhí)行。

  3. volatile變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作;

  4. 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;

  5. 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作;

  6. 線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生;

  7. 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行;

  8. 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開(kāi)始;

其中,傳遞規(guī)則我加粗了,這個(gè)規(guī)則至關(guān)重要。如何熟練的使用傳遞規(guī)則是實(shí)現(xiàn)同步的關(guān)鍵。

然后,再換個(gè)角度解釋 HB:當(dāng)一個(gè)操作 A HB 操作 B,那么,操作 A 對(duì)共享變量的操作結(jié)果對(duì)操作 B 都是可見(jiàn)的。

同時(shí),如果 操作 B HB 操作 C,那么,操作 A 對(duì)共享變量的操作結(jié)果對(duì)操作 B 都是可見(jiàn)的。

而實(shí)現(xiàn)可見(jiàn)性的原理則是 cache protocol 和 memory barrier。通過(guò)緩存一致性協(xié)議和內(nèi)存屏障實(shí)現(xiàn)可見(jiàn)性。

如何實(shí)現(xiàn)同步?

在 Doug Lea 著作 《Java Concurrency in Practice》中,有下面的描述:

Java如何使用happens-before規(guī)則實(shí)現(xiàn)共享變量的同步操作

書(shū)中提到:通過(guò)組合 hb 的一些規(guī)則,可以實(shí)現(xiàn)對(duì)某個(gè)未被鎖保護(hù)變量的可見(jiàn)性。

但由于這個(gè)技術(shù)對(duì)語(yǔ)句的順序很敏感,因此容易出錯(cuò)。

樓主接下來(lái),將演示如何通過(guò) volatile 規(guī)則和程序次序規(guī)則實(shí)現(xiàn)對(duì)一個(gè)變量同步。

來(lái)一個(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é)肯定要說(shuō)了,這個(gè) num 變量沒(méi)有使用 volatile,會(huì)有可見(jiàn)性問(wèn)題,即:t1 線程更新了 num,t2 線程無(wú)法感知。

哈哈,樓主剛開(kāi)始也是這么認(rèn)為的,但最近通過(guò)研究 HB 規(guī)則,我發(fā)現(xiàn),去掉 num 的 volatile 修飾也是可以的。

我們分析一下,樓主畫(huà)了一個(gè)圖:

Java如何使用happens-before規(guī)則實(shí)現(xiàn)共享變量的同步操作

我們分析這個(gè)圖:

  1. 首先,紅色和黃色表示不同的線程操作。

  2. 紅色線程對(duì) num 變量做 ++,然后修改了 volatile 變量,這個(gè)是符合 程序次序規(guī)則的。也就是 1 HB 2.

  3. 紅色線程對(duì) volatile 的寫(xiě) HB 黃色線程對(duì) volatile 的讀,也就是 2 HB 3.

  4. 黃色線程讀取 volatile 變量,然后對(duì) num 變量做 ++,符合 程序次序規(guī)則,也就是 3 HB 4.

  5. 根據(jù)傳遞性規(guī)則,1 肯定 HB 4. 所以,1 的修改對(duì) 4來(lái)說(shuō)都是可見(jiàn)的。

注意:HB 規(guī)則保證上一個(gè)操作的結(jié)果對(duì)下一個(gè)操作都是可見(jiàn)的。

所以,上面的小程序中,線程 A 對(duì) num 的修改,線程 B 是完全感知的 —— 即使 num 沒(méi)有使用 volatile 修飾。

這樣,我們就借助 HB 原則實(shí)現(xiàn)了對(duì)一個(gè)變量的同步操作,也就是在多線程環(huán)境中,保證了并發(fā)修改共享變量的安全性。并且沒(méi)有對(duì)這個(gè)變量使用 Java 的原語(yǔ):volatile 和 synchronized 和 CAS(假設(shè)算的話)。

這可能看起來(lái)不安全(實(shí)際上安全),也好像不太容易理解。因?yàn)檫@一切都是 HB 底層的 cache protocol 和 memory barrier 實(shí)現(xiàn)的。

其他規(guī)則實(shí)現(xiàn)同步

  1. 利用線程終結(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();
  }

    2.利用線程 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 的可見(jiàn)性。

確實(shí)有點(diǎn)顛覆之前的觀念。之前的觀念中,如果一個(gè)變量沒(méi)有被 volatile 修飾或 final 修飾,那么他在多線程下的讀寫(xiě)肯定是不安全的 —— 因?yàn)闀?huì)有緩存,導(dǎo)致讀取到的不是最新的。

然而,通過(guò)借助 HB,我們可以實(shí)現(xiàn)。

總結(jié)

雖然本文標(biāo)題是通過(guò) happens-before 實(shí)現(xiàn)對(duì)共享變量的同步操作,但主要目的還是更深刻的理解 happen-before,理解他的 happens-before 概念其實(shí)就是保證多線程環(huán)境中,上一個(gè)操作對(duì)下一個(gè)操作的有序性和操作結(jié)果的可見(jiàn)性。

同時(shí),通過(guò)靈活的使用傳遞性規(guī)則,再對(duì)規(guī)則進(jìn)行組合,就可以將兩個(gè)線程進(jìn)行同步 —— 實(shí)現(xiàn)指定的共享變量不使用原語(yǔ)也可以保證可見(jiàn)性。雖然這好像不是很易讀,但也是一種嘗試。

關(guān)于如何組合使用規(guī)則實(shí)現(xiàn)同步,Doug Lea 在 JUC 中給出了實(shí)踐。

例如老版本的 FutureTask 的內(nèi)部類 Sync(已消失),通過(guò) tryReleaseShared 方法修改 volatile 變量,tryAcquireShared 讀取 volatile 變量,這是利用了 volatile 規(guī)則;

通過(guò)在 tryReleaseShared 之前設(shè)置非 volatile 的 result 變量,然后在 tryAcquireShared 之后讀取 result 變量,這是利用了程序次序規(guī)則。

從而保證 result 變量的可見(jiàn)性。和我們的第一個(gè)例子類似:利用程序次序規(guī)則和 volatile 規(guī)則實(shí)現(xiàn)普通變量可見(jiàn)性。

而 Doug Lea 自己也說(shuō)了,這個(gè)“借助”技術(shù)非常容易出錯(cuò),要謹(jǐn)慎使用。但在某些情況下,這種“借助”是非常合理的。

實(shí)際上,BlockingQueue 也是“借助”了 happens-before 的規(guī)則。還記得 unlock 規(guī)則嗎?當(dāng) unlock 發(fā)生后,內(nèi)部元素一定是可見(jiàn)的。

而類庫(kù)中還有其他的操作也“借助”了 happens-before 原則:并發(fā)容器,CountDownLatch,Semaphore,F(xiàn)uture,Executor,CyclicBarrier,Exchanger 等。

總而言之,言而總之:

happens-before 原則是 JMM 的核心所在,只有滿足了 hb 原則才能保證有序性和可見(jiàn)性,否則編譯器將會(huì)對(duì)代碼重排序。hb 甚至將 lock 和 volatile 也定義了規(guī)則。

通過(guò)適當(dāng)?shù)膶?duì) hb 規(guī)則的組合,可以實(shí)現(xiàn)對(duì)普通共享變量的正確使用。

以上是“Java如何使用happens-before規(guī)則實(shí)現(xiàn)共享變量的同步操作”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI