JAVA記憶體模型和Happens-Before規則

王子發表於2020-11-18

 

前言

上一篇文章王子給大家介紹了併發程式設計中比較關心的三個核心問題,可見性、有序性和原子性

今天我們繼續來探索併發程式設計的內容,聊一聊JAVA的記憶體模型和Happens-Before規則。

 

JAVA記憶體模型

這裡的JAVA記憶體模型指的不是我們JVM專欄中提到的記憶體分佈模型,而是針對併發程式設計的,小夥伴們不要混淆概念了。

我們已經知道,導致可見性問題的是快取,導致有序性問題的是指令重排,那麼禁用快取和禁用指令重排不就可以避免出現這兩種問題了嗎。

但想想也知道,如果直接禁用掉,效能會大打折扣,所以正確的方式應該是按需禁用

只有程式設計師才能分析出什麼時候應該禁用,所以為了解決可見性和有序性,其實只要提供給程式設計師按需禁用的API介面就可以了。

JAVA的記憶體模型是一個很複雜的規範,可以從不同的角度來解釋,本質上我們可以理解成JAVA記憶體模型規範了JVM如何按需禁用快取和禁用指令重排。

具體來說這些方法包括 volatilesynchronized 和 final 等關鍵字,以及六項 Happens-Before 規則。

volatile不是JAVA獨有的關鍵字,它最開始的含義就是禁用CPU快取,JAVA1.5之後對它進行了語義加強,就是引入了一套Happens-Before 規則。

例如下面的程式碼:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 這裡 x 會是多少呢?
    }
  }
}

假如執行緒A執行了writer方法,執行緒B執行reader方法,如果執行緒B發現了v=true,那麼同時也會發現x=42。

 

Happens-Before 規則

接下來我們就來看看今天的主角,Happens-Before是什麼?

Happens-Before要表達的是:前面一個操作的結果對後續操作是可見的,它約束了編譯器的優化行為,雖允許編譯器優化導致的指令重排,但是要求編譯器優化後一定遵守 Happens-Before 規則。

都說Happens-Before對於JAVA記憶體模型來講是一個比較晦澀難懂的部分,但我們一點一點來剖析,其實沒那麼難理解。

程式的順序性規則

這條規則是指在一個執行緒中,按照程式順序,前面的操作 Happens-Before 於後續的任意操作。

這條規則還是比較容易理解的,就是保證了單執行緒中程式的順序性。

volatile變數規則

這條規則是指對一個 volatile 變數的寫操作, Happens-Before 於後續對這個 volatile 變數的讀操作。

這麼看的話,是不是發現其實它就是禁用CPU快取的意思,多執行緒下保證變數的可見性。

傳遞性

這條規則是指如果 A Happens-Before B,且 B Happens-Before C,那麼 A Happens-Before C。

這個傳遞性也很好理解,那麼假如把傳遞性和volatile變數規則放在一起會發生什麼呢?

就比如我們上文中的程式碼,x=42 Happens-Before v=true,寫變數v=true Happens-Before 讀變數v,那麼根據傳遞性規則,x=42 Happens-Before 讀變數v。

所以我們之前分析,如果執行緒B讀變數v=true,那麼x=42對於執行緒B也是可見的。

併發工具包(java.util.concurrent)就是靠 volatile 語義來搞定可見性的,同時傳遞性也是對volatile關鍵字的增強,保證了可見性的同時也保證了有序性。

管程中鎖的規則

這條規則是指對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。

這條規則其實也很容易理解,不加鎖何來解鎖一說。

執行緒start()規則

這條是關於執行緒啟動的。它是指主執行緒 A 啟動子執行緒 B 後,子執行緒 B 能夠看到主執行緒在啟動子執行緒 B 前的操作。

這條規則也沒什麼好解釋的,就是字面意思。

執行緒join()規則

這條是關於執行緒等待的。它是指主執行緒 A 等待子執行緒 B 完成(主執行緒 A 通過呼叫子執行緒 B 的 join() 方法實現),當子執行緒 B 完成後(主執行緒 A 中 join() 方法返回),主執行緒能夠看到子執行緒的操作。當然所謂的“看到”,指的是對共享變數的操作。

 

總結

Java 的記憶體模型是併發程式設計領域的一次重要創新,它主要分為兩部分,一部分面向編寫併發程式的應用開發人員,另一部分是面向 JVM 的實現人員的。

我們在併發專欄中理解前者就可以了。

晦澀難懂的Happens-Before原則,看完本文你覺得它還有那麼難嗎?

 

往期文章推薦:

JVM專欄

訊息中介軟體專欄

併發程式設計專欄

 

相關文章