欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java利用happen-before規(guī)則如何實現共享變量的同步操作詳解

 更新時間:2018年06月03日 10:34:27   作者:莫那-魯道  
這篇文章主要給大家介紹了關于Java利用happen-before規(guī)則實現共享變量的同步操作的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用java具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

前言

熟悉 Java 并發(fā)編程的都知道,JMM(Java 內存模型) 中的 happen-before(簡稱 hb)規(guī)則,該規(guī)則定義了 Java 多線程操作的有序性和可見性,防止了編譯器重排序對程序結果的影響。

Java語言中有一個“先行發(fā)生”(happen—before)的規(guī)則,它是Java內存模型中定義的兩項操作之間的偏序關系,如果操作A先行發(fā)生于操作B,其意思就是說,在發(fā)生操作B之前,操作A產生的影響都能被操作B觀察到,“影響”包括修改了內存中共享變量的值、發(fā)送了消息、調用了方法等,它與時間上的先后發(fā)生基本沒有太大關系。這個原則特別重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。

按照官方的說法:

當一個變量被多個線程讀取并且至少被一個線程寫入時,如果讀操作和寫操作沒有 HB 關系,則會產生數據競爭問題。

要想保證操作 B 的線程看到操作 A 的結果(無論 A 和 B 是否在一個線程),那么在 A 和 B 之間必須滿足 HB 原則,如果沒有,將有可能導致重排序。

當缺少 HB 關系時,就可能出現重排序問題。

HB 有哪些規(guī)則?

這個大家都非常熟悉了應該,大部分書籍和文章都會介紹,這里稍微回顧一下:

  • 程序次序規(guī)則:一個線程內,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
  • 鎖定規(guī)則:在監(jiān)視器鎖上的解鎖操作必須在同一個監(jiān)視器上的加鎖操作之前執(zhí)行。
  • volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作;
  • 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;
  • 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作;
  • 線程中斷規(guī)則:對線程interrupt()方法的調用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生;
  • 線程終結規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執(zhí)行;
  • 對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始;

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

然后,再換個角度解釋 HB:當一個操作 A HB 操作 B,那么,操作 A 對共享變量的操作結果對操作 B 都是可見的。

同時,如果 操作 B HB 操作 C,那么,操作 A 對共享變量的操作結果對操作 B 都是可見的。

而實現可見性的原理則是 cache protocol 和 memory barrier。通過緩存一致性協(xié)議和內存屏障實現可見性。

如何實現同步?

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

書中提到:通過組合 hb 的一些規(guī)則,可以實現對某個未被鎖保護變量的可見性。

但由于這個技術對語句的順序很敏感,因此容易出錯。

樓主接下來,將演示如何通過 volatile 規(guī)則和程序次序規(guī)則實現對一個變量同步。

來一個熟悉的例子:

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 的數字。

熟悉并發(fā)編程的同學肯定要說了,這個 num 變量沒有使用 volatile,會有可見性問題,即:t1 線程更新了 num,t2 線程無法感知。

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

我們分析一下,樓主畫了一個圖:

我們分析這個圖:

  • 首先,紅色和黃色表示不同的線程操作。
  • 紅色線程對 num 變量做 ++,然后修改了 volatile 變量,這個是符合 程序次序規(guī)則的。也就是 1 HB 2.
  • 紅色線程對 volatile 的寫 HB 黃色線程對 volatile 的讀,也就是 2 HB 3.
  • 黃色線程讀取 volatile 變量,然后對 num 變量做 ++,符合 程序次序規(guī)則,也就是 3 HB 4.
  • 根據傳遞性規(guī)則,1 肯定 HB 4. 所以,1 的修改對 4來說都是可見的。

注意:HB 規(guī)則保證上一個操作的結果對下一個操作都是可見的。

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

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

這可能看起來不安全(實際上安全),也好像不太容易理解。因為這一切都是 HB 底層的 cache protocol 和 memory barrier 實現的。

其他規(guī)則實現同步

利用線程終結規(guī)則實現:

 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ī)則實現:

 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,我們可以實現。

總結

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

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

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

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

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

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

而 Doug Lea 自己也說了,這個“借助”技術非常容易出錯,要謹慎使用。但在某些情況下,這種“借助”是非常合理的。

實際上,BlockingQueue 也是“借助”了 happen-before 的規(guī)則。還記得 unlock 規(guī)則嗎?當 unlock 發(fā)生后,內部元素一定是可見的。

而類庫中還有其他的操作也“借助”了 happen-before 原則:并發(fā)容器,CountDownLatch,Semaphore,Future,Executor,CyclicBarrier,Exchanger 等。

總而言之,言而總之:

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

通過適當的對 hb 規(guī)則的組合,可以實現對普通共享變量的正確使用。

好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

相關文章

  • Springboot結合rabbitmq實現的死信隊列

    Springboot結合rabbitmq實現的死信隊列

    為了保證訂單業(yè)務的消息數據不丟失,需要使用到RabbitMQ的死信隊列機制,本文主要介紹了Springboot結合rabbitmq實現的死信隊列,具有一定的參考價值,感興趣的可以了解一下
    2023-09-09
  • Java封裝好的mail包發(fā)送電子郵件的類

    Java封裝好的mail包發(fā)送電子郵件的類

    本文給大家分享了2個java封裝好的mail包發(fā)送電子郵件的類,并附上使用方法,小伙伴們可以根據自己的需求自由選擇。
    2016-01-01
  • java?Collection集合接口的介紹和使用詳解

    java?Collection集合接口的介紹和使用詳解

    這篇文章主要為大家介紹了java?Collection集合接口的介紹和使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-07-07
  • JAVA基于SnakeYAML實現解析與序列化YAML

    JAVA基于SnakeYAML實現解析與序列化YAML

    這篇文章主要介紹了JAVA基于SnakeYAML實現解析與序列化YAML,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2019-12-12
  • MyBatis如何進行雙重foreach循環(huán)

    MyBatis如何進行雙重foreach循環(huán)

    這篇文章主要介紹了MyBatis如何進行雙重foreach循環(huán),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-02-02
  • JWT在OpenFeign調用中進行令牌中繼詳解

    JWT在OpenFeign調用中進行令牌中繼詳解

    Feign是一個聲明式的Web Service客戶端,是一種聲明式、模板化的HTTP客戶端。而OpenFeign是Spring Cloud 在Feign的基礎上支持了Spring MVC的注解,如@RequesMapping等等,這篇文章主要給大家介紹了關于JWT在OpenFeign調用中進行令牌中繼的相關資料,需要的朋友可以參考下
    2021-10-10
  • Spring Boot2讀取配置常用方法代碼實例

    Spring Boot2讀取配置常用方法代碼實例

    這篇文章主要介紹了Spring Boot2讀取配置常用方法代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-11-11
  • 論java如何通過反射獲得方法真實參數名及擴展研究

    論java如何通過反射獲得方法真實參數名及擴展研究

    這篇文章主要為大家介紹了java如何通過反射獲得方法的真實參數名以及擴展研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助祝大家多多進步早日升職加薪
    2022-01-01
  • Java自定義函數調用方法解析

    Java自定義函數調用方法解析

    這篇文章主要介紹了java自定義函數調用方法解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2019-11-11
  • Redis實現延遲隊列的全流程詳解

    Redis實現延遲隊列的全流程詳解

    Redisson是Redis服務器上的分布式可伸縮Java數據結構,這篇文中主要為大家介紹了Redisson實現的優(yōu)雅的延遲隊列的方法,需要的可以參考一下
    2023-03-03

最新評論