分析JVM線程同步原理

線程和共享數據

Java 的一個優勢就是在語言層面支持多線程,這種支持集中在協調多線程對數據的訪問上。java

JVM 將運行時數據劃分爲幾個區域:一個或多個棧,一個堆,一個方法區。數組

在 JVM 中,每一個線程擁有一個棧,其餘線程沒法訪問,裏面的數據包括:局部變量,函數參數,線程調用的方法的返回值。棧裏面的數據只包含原生數據類型和對象引用。在 JVM 中,不可能將實際對象的拷貝放入棧。全部對象都在堆裏面。多線程

JVM 只有一個堆,全部線程都共享它。堆中只包含對象,把單獨的原生類型或者對象引用放入堆也是不可能的,除非它們是對象的一部分。數組也在堆中,包括原生類型的數組,由於在 Java 中,數組也是對象。函數

除了棧和堆,另外一個存放數據的區域就是方法區了,它包含程序中使用到的全部類(靜態)變量。方法區相似於棧,也只包含原生類型和對象引用,可是又跟棧不一樣,方法區中類變量是線程共享的。this

對象鎖和類鎖

正如前面所說,JVM 中的兩個區域包含線程共享的數據,分別是:spa

  1. :包含全部對象
  2. 方法區:包含全部類變量

若是多個線程須要同時使用同一個對象或者類變量,它們對數據的訪問必須被恰當地控制。不然,程序會產生不可預測的行爲。線程

爲了協調多個線程對共享數據的訪問,JVM 給每一個對象和類關聯了一個鎖。鎖就像是任意時間點只有一個線程可以擁有的特權。若是一個線程想要鎖住一個特定的對象或者類,它須要向 JVM 請求鎖。線程向 JVM 請求鎖以後,可能很快就拿到,或者過一會就拿到,也可能永遠拿不到。當線程不須要鎖以後,它把鎖還給 JVM。若是其餘線程須要這個鎖,JVM 會交給該線程。code

類鎖的實現其實跟對象鎖是同樣的。當 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 要求獲得類鎖。在同步方法完成以後,無論它是正常返回仍是拋出異常,鎖都會被釋放。

相關文章
相關標籤/搜索