併發編程之java鎖的升級與對比

前言:
在併發編程中,經常用到synchronized關鍵詞,總是感覺使用它會很重。隨着Java SE 1.6對synchronize進行了各種優化,引入了偏向鎖和輕量級鎖,在某些情況下,減少了獲得鎖和釋放鎖帶來得性能消耗。

一、文章導圖

圖片描述

二、鎖的升級與對比

1、synchronized實現同步的基礎

java中每個對象都可以作爲一個鎖,具體的表現有以下三種形式:

  • 普通方法同步,鎖爲當前實例對象
  • 靜態方法同步,鎖爲當前類的Class對象
  • 方法塊同步,鎖爲synchronized後括號中填寫的對象

當一個線程試圖訪問同步代碼塊時,必須首先獲取到鎖,退出同步代碼塊時或拋出異常必須釋放鎖。
JVM基於進入與退出Monitor對象實現方法同步與代碼塊同步,不過兩者的實現細節不太一樣,可參見如下字節碼所示。

public class SynchronizedDemo {

    /**
     * 同步方法
     */
    public synchronized void testSynchronizedMethod () {
        System.out.println("test synchronized method");
    }

    /**
     * 同步靜態方法
     */
    public synchronized static void testSynchronizedStaticMethod () {
        System.out.println("test synchronized static method");
    }

    /**
     * 方法同步塊
     */
    public void testSynchronizedMethodBlock() {
        synchronized (this) {
            System.out.println("test synchronized method block");
        }
    }

}

進入java文件所在目錄,通過命令行進行編譯:javac SynchronizedDemo.java
然後同目錄下通過如下命令,進行查看編譯後字節碼的詳細信息:javap -verbose SynchronizedDemo.class
圖片描述
如圖,任何對象有一個Monitor與之對應,線程執行到monitorenter時會嘗試獲取Monitor對象的所有權,即嘗試獲取對象上的鎖。

Monitor作爲操作系統的一種原語,具體由相應的編程語言實現。每個Monitor對象又包括:

  • _owner:記錄當前持有的鎖的線程,也可以了理解成鎖的臨界區
  • _entrySet:一個隊列,記錄所有阻塞等待鎖的線程
  • _waitSet:一個隊列,記錄所有調用wait未被喚醒的線程

當一個線程訪問Object鎖時,會被放入_entrySet中等待,如果該線程獲取到鎖,成爲當前鎖的_owner;期間,線程邏輯上缺少外部條件時,線程通過調用wait方法釋放鎖,進入到_waitSet隊列,等到條件滿足時,又被喚醒與_entrySet一起競爭_owner;這個外部條件在monitor機制中稱爲條件變量。

2、java對象頭

Java對象包括了對象頭、屬性字段、補齊區域等。
對象頭在最前端,包括了兩部分(非數組類型)或三部分(數組類型,多存在數據的長度),結構如下所示

長度(32位機/64位機 bit) 內容 說明
32/64 Mark Word 存儲對象的hashCode和鎖信息等
32/64 Class Metadata Address 存儲帶對象類型數據的指針
32/32 Array Length 數組的長度(如果對象是數組)
對象頭的Mark Word會有指向管程Monitor的指針。
補齊區域:由於JVM要求java的對象佔的內存大小應該是8bit的倍數,所以會有幾個字節用於把對象的大小補齊到8bit的倍數,沒有其它特別功能。

其中Mark Word的存儲數據隨着鎖標誌的變化如下:
圖片描述

3、偏向鎖

java SE 1.6引入偏向鎖與輕量級鎖後,鎖一共有4中狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖和重量級鎖狀態。且鎖會隨着競爭情況逐步升級,但不可降級(基於JVM的一個假定:“假定一旦破壞了上一級鎖的升級,就認爲該假定以後也不成龍”)。

爲了讓線程獲取鎖的代價更低而引入偏向鎖,因爲多線程中,有些情況下,獲取鎖的線程同時只會有一個。
如下,線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。

圖片描述

  1. 線程1訪問同步代碼塊,確定鎖的標誌爲01,非偏向對象時,會嘗試CAS競爭
  2. 競爭成功後,將鎖對象頭的Mark Word中的線程ID指向自己,此時鎖的標誌爲01,爲偏向鎖
  3. 執行訪問體
  4. 此時線程2訪問同步塊,確定鎖的標誌爲01,爲偏向對象時,會嘗試CAS將對象頭的偏向鎖指向當前線程2
  5. 替換失敗(線程1偏向鎖),開始撤銷偏向鎖
  6. 待到全局安全點,暫停線程1(原持有偏向鎖的線程),如果線程1方法體執行完或處於未活動狀態,則將線程ID置空,此時處於無鎖狀態
  7. 恢復線程1(原持有偏向鎖的線程);偏向鎖偏向線程2。
偏向鎖默認是開啓的,可使用JVM參數關閉:-XX:-UseBiasedLocking,那麼程序默認會進入輕量級鎖

4、輕量級鎖

引入輕量級鎖,爲了不申請互斥量,包括系統調用引起的內核態與用戶態的切換、線程阻塞造成的線程切換等。

在線程中,虛擬機會在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱Displaced Mark Word。
如下,線程1與線程2演示了輕量級鎖膨脹爲重量級鎖的流程。

圖片描述

  1. 線程1訪問同步代碼塊,確定鎖的標誌爲01(偏向鎖升級或偏向鎖關閉),進行獲取輕量級鎖,線程2同理
  2. 線程1分配本線程棧的鎖記錄空間,並拷貝鎖對象的Mark Word到當前線程棧的鎖記錄中
  3. 線程2分配本線程棧的鎖記錄空間,並拷貝鎖對象的Mark Word到當前線程棧的鎖記錄中
  4. 線程1嘗試使用CAS替換鎖對象頭的Mark Word指向鎖記錄的指針,成功後,線程1獲取到輕量級鎖
  5. 線程2嘗試使用CAS替換鎖對象頭的Mark Word指向鎖記錄的指針,失敗,因爲線程已獲得鎖,此時線程2自旋
  6. 線程2自旋一定次數後,失敗,鎖膨脹爲重量級鎖,並阻塞本線程(線程2)
  7. 線程1同步方法體執行完,CAS替換Mark Word,失敗,因爲線程2在競爭鎖資源
  8. 線程1釋放鎖並喚醒等待的線程,等待的線程2被喚醒,重新爭奪訪問同步塊。

5、重量級鎖

內置鎖在java中被抽象爲監視器鎖(monitor),對於重量級鎖,監視器鎖直接對應底層操作系統中的互斥量(mutex),這種同步成本非常高,包括系統調用引起的內核態與用戶態切換、線程阻塞造成的線程切換等。

關於不同鎖的優缺點對比,如下所示

有點 缺點 使用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法時相比僅存在納秒級的差距;畢竟僅第一執行CAS操作 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步的場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度;相比偏向鎖,獲取和釋放鎖均執行一次CAS操作 如果使用得不到鎖競爭的線程,會使用自旋會消耗CPU資源 追求響應時間,同步塊執行速度非常快
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊執行速度較長
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章