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

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

1、文章導圖

圖片描述

2、鎖的升級與對比

一、synchronized實現同步的基礎

java中每一個對象均可以做爲一個鎖,具體的表現有如下三種形式:前端

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

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

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機制中稱爲條件變量。安全

二、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的存儲數據隨着鎖標誌的變化以下:
圖片描述併發

三、偏向鎖

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,那麼程序默認會進入輕量級鎖

四、輕量級鎖

引入輕量級鎖,爲了避免申請互斥量,包括系統調用引發的內核態與用戶態的切換、線程阻塞形成的線程切換等。優化

在線程中,虛擬機會在當前線程的棧幀中創建一個名爲鎖記錄(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指向鎖記錄的指針,失敗,由於線程1得到鎖,此時線程2自旋
  6. 線程2自旋必定次數後,失敗,鎖膨脹爲重量級鎖,並阻塞本線程(線程2)
  7. 線程1同步方法體執行完,CAS替換Mark Word,失敗,由於線程2在競爭鎖資源
  8. 線程1釋放鎖並喚醒等待的線程,等待的線程2被喚醒,從新爭奪訪問同步塊。

五、重量級鎖

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

關於不一樣鎖的優缺點對比,以下所示

有點 缺點 使用場景
偏向鎖 加鎖和解鎖不須要額外的消耗,和執行非同步方法時相比僅存在納秒級的差距;畢竟僅第一執行CAS操做 若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步的場景
輕量級鎖 競爭的線程不會阻塞,提升了程序的響應速度;相比偏向鎖,獲取和釋放鎖均執行一次CAS操做 若是使用得不到鎖競爭的線程,會使用自旋會消耗CPU資源 追求響應時間,同步塊執行速度很是快
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊執行速度較長
相關文章
相關標籤/搜索