JVM 進行執行緒同步背後的原理

Yonah-瀟發表於2016-07-21

前言

所有的 Java 程式都會被翻譯為包含位元組碼的 class 檔案,位元組碼是 JVM 的機器語言。這篇文章將闡述 JVM 是如何處理執行緒同步以及相關的位元組碼。

執行緒和共享資料

Java 的一個優點就是在語言層面支援多執行緒,這種支援集中在協調多執行緒對資料的訪問上。

JVM 將執行時資料劃分為幾個區域:一個或多個棧,一個堆,一個方法區。

在 JVM 中,每個執行緒擁有一個棧,其他執行緒無法訪問,裡面的資料包括:區域性變數,函式引數,執行緒呼叫的方法的返回值。棧裡面的資料只包含原生資料型別和物件引用。在 JVM 中,不可能將實際物件的拷貝放入棧。所有物件都在堆裡面。

JVM 只有一個堆,所有執行緒都共享它。堆中只包含物件,把單獨的原生型別或者物件引用放入堆也是不可能的,除非它們是物件的一部分。陣列也在堆中,包括原生型別的陣列,因為在 Java 中,陣列也是物件。

除了棧和堆,另一個存放資料的區域就是方法區了,它包含程式中使用到的所有類(靜態)變數。方法區類似於棧,也只包含原生型別和物件引用,但是又跟棧不同,方法區中類變數是執行緒共享的。

物件鎖和類鎖

正如前面所說,JVM 中的兩個區域包含執行緒共享的資料,分別是:

  1. :包含所有物件
  2. 方法區:包含所有類變數

如果多個執行緒需要同時使用同一個物件或者類變數,它們對資料的訪問必須被恰當地控制。否則,程式會產生不可預測的行為。

為了協調多個執行緒對共享資料的訪問,JVM 給每個物件和類關聯了一個鎖。鎖就像是任意時間點只有一個執行緒能夠擁有的特權。如果一個執行緒想要鎖住一個特定的物件或者類,它需要向 JVM 請求鎖。執行緒向 JVM 請求鎖之後,可能很快就拿到,或者過一會就拿到,也可能永遠拿不到。當執行緒不需要鎖之後,它把鎖還給 JVM。如果其他執行緒需要這個鎖,JVM 會交給該執行緒。

類鎖的實現其實跟物件鎖是一樣的。當 JVM 載入類檔案的時候,它會建立一個對應類java.lang.Class物件。當你鎖住一個類的時候,你實際上是鎖住了這個類的Class物件。

執行緒訪問物件例項或者類變數的時候不需要獲取鎖。但是如果一個執行緒獲取了一個鎖,其他執行緒不能訪問被鎖住的資料,直到擁有鎖的執行緒釋放它。

管程

JVM 使用鎖和管程協作。管程監視一段程式碼,保證一個時間點內只有一個執行緒能執行這段程式碼。

每個管程與一個物件引用關聯。當執行緒到達管程監視程式碼段的第一條指令時,執行緒必須獲取關聯物件的鎖。執行緒不能執行這段程式碼直到它得到了鎖。一旦它得到了鎖,執行緒可以進入被保護的程式碼段。

當執行緒離開被保護的程式碼塊,不管是如何離開的,它都會釋放關聯物件的鎖。

多次鎖定

一個執行緒被允許鎖定一個物件多次。對於每個物件,JVM 維護了一個鎖的計數器。沒有被鎖的物件計數為 0。當一個執行緒第一次獲取鎖,計數器自增變為 1。每次這個執行緒(已經得到鎖的執行緒)請求同一個物件的鎖,計數器都會自增。每次執行緒釋放鎖,計數器都會自減。當計數器變為 0 時,鎖才被釋放,可以給別的執行緒使用。

同步塊

在 Java 語言的術語中,協調多個執行緒訪問共享資料被稱為同步(synchronization)。Java 提供了兩種內建的方式來同步對資料的訪問:

  1. 同步語句
  2. 同步方法

同步語句

為了建立同步語句,你需要使用synchronized關鍵字,括號裡面是同步的物件引用,如下所示:

class KitchenSync {
    private int[] intArray = new int[10];
    void reverseOrder() {
        synchronized (this) {
            int halfWay = intArray.length / 2;
            for (int i = 0; i < halfWay; ++i) {
                int upperIndex = intArray.length - 1 - i;
                int save = intArray[upperIndex];
                intArray[upperIndex] = intArray[i];
                intArray[i] = save;
            }
        }
    }
}

在上面的例子中,被同步塊包含的語句不會被執行,直到執行緒得到this引用的物件鎖。如果不是鎖住this引用,而是鎖住其他物件,線上程執行同步塊語句之前,它需要獲得該物件的鎖。

有兩個位元組碼monitorentermonitorexit,被用來同步方法中的同步塊

位元組碼 運算元 描述
monitorenter 取出物件引用,請求與物件引用關聯的鎖
monitorexit 取出物件引用,釋放與物件引用關聯的鎖

monitorenter被 JVM 執行時,它請求棧頂物件引用關聯的鎖。如果該執行緒已經擁有該物件的鎖,計數器自增。每次monitorexit被執行,計數器自減。當計數器變為 0 時,該鎖被釋放。

注意:當同步塊中丟擲異常時,catch語句保證物件鎖被釋放。不管同步塊是如何退出的,JVM 保證執行緒會釋放鎖。

同步方法

為了同步整個方法,你只需要在方法宣告前面加上synchronized關鍵字。

class HeatSync {
    private int[] intArray = new int[10];
    synchronized void reverseOrder() {
        int halfWay = intArray.length / 2;
        for (int i = 0; i < halfWay; ++i) {
            int upperIndex = intArray.length - 1 - i;
            int save = intArray[upperIndex];
            intArray[upperIndex] = intArray[i];
            intArray[i] = save;
        }
    }
}

JVM 不會使用特殊的位元組碼來呼叫同步方法。當 JVM 解析方法的符號引用時,它會判斷方法是不是同步的。如果是,JVM 要求執行緒在呼叫之前請求鎖。對於例項方法,JVM 要求得到該例項物件的鎖。對於類方法,JVM 要求得到類鎖。在同步方法完成之後,不管它是正常返回還是丟擲異常,鎖都會被釋放。

相關文章