Java 8 併發篇 - 冷靜分析 Synchronized(下)

魔王不造反發表於2018-04-09

4.Synchronized實現原理

4.1 Synchronization

Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions).
For code written in the Java programming language, perhaps the most common form of synchronization is the synchronized method. A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the run-time constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions (§2.11.10).
  • 本段摘自 The Java® Virtual Machine Specification 3.14. Synchronization
  • 在JVM中,同步的實現是通過監視器鎖的進入和退出實現的,要麼顯示得通過monitorenter 和 monitorexit指令實現,要麼隱示地通過方法呼叫和返回指令實現
  • 對於Java程式碼來說,或許最常用的同步實現就是同步方法。其中同步程式碼塊是通過使用 monitorenter 和 monitorexit 實現的,而同步方法卻是使用 ACC_SYNCHRONIZED 標記符隱示的實現,原理是通過方法呼叫指令檢查該方法在常量池中是否包含 ACC_SYNCHRONIZED 標記符
  • 本篇不會針對Synchronized的位元組碼實現進行分析,只是點到為止,有興趣的讀者可參見 JVM原始碼分析之synchronized實現 (當然,若有機會開JVM番的話,筆者會重新分析的)

4.2 反編譯

4.2.1 預準備

為了能直觀瞭解Synchronized的工作原理,我們通過反編譯SynchronizedDeme類的class檔案的方式看看都發生了什麼
package concurrent;
public class SynchronizedDemo {
    public static synchronized void staticMethod() throws InterruptedException {
        System.out.println("靜態同步方法開始");
        Thread.sleep(1000);
        System.out.println("靜態同步方法結束");
    }
    public synchronized void method() throws InterruptedException {
        System.out.println("例項同步方法開始");
        Thread.sleep(1000);
        System.out.println("例項同步方法結束");
    }
    public synchronized void method2() throws InterruptedException {
        System.out.println("例項同步方法2開始");
        Thread.sleep(3000);
        System.out.println("例項同步方法2結束");
    }
    public static void main(String[] args) {
        final SynchronizedDemo synDemo = new SynchronizedDemo();
        Thread thread1 = new Thread(() -> {
            try {
               synDemo.method();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                synDemo.method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        thread2.start();
    }
}
複製程式碼

4.2.1 生成.class檔案

javac SynchronizedDemo.java 
複製程式碼
注意:由於筆者OS的預設編碼方式是UTF-8,因此可能出現以下錯誤
Java 8 併發篇 - 冷靜分析 Synchronized(下)

解決方案如下:只要通過 -encoding 指定指明編碼方式即可

javac -encoding UTF-8 SynchronizedDemo.java 
複製程式碼

最終我們將得到一個 .class 檔案,即 SynchronizedDemo.class

4.2.2 javap反編譯

javap -v SynchronizedDemo 
複製程式碼

通過反編譯我們會得到常量池、同步方法、同步程式碼塊的不同編譯結果,之後我們將基於這三個進行介紹

常量池圖示

Java 8 併發篇 - 冷靜分析 Synchronized(下)

常量池除了會包含基本型別和字串及陣列的常量值外,還包含以文字形式出現的符號引用:

  • 類和介面的全限定名
  • 欄位的名稱和描述符
  • 方法和名稱和描述符

同步方法圖示

Java 8 併發篇 - 冷靜分析 Synchronized(下)

同步方法會包含一個ACC_SYNCHCRONIZED標記符

同步程式碼塊圖示

Java 8 併發篇 - 冷靜分析 Synchronized(下)

同步程式碼塊會在程式碼中插入 monitorenter 和 monitorexist 指令

4.3 同步程式碼塊同步原理

4.3.1 monitor監視器

  • 每個物件都有一個監視器,在同步程式碼塊中,JVM通過monitorenter和monitorexist指令實現同步鎖的獲取和釋放功能
  • 當一個執行緒獲取同步鎖時,即是通過獲取monitor監視器進而等價為獲取到鎖
  • monitor的實現類似於作業系統中的管程

4.3.2 monitorenter指令

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
  • 每個物件都有一個監視器。當該監視器被佔用時即是鎖定狀態(或者說獲取監視器即是獲得同步鎖)。執行緒執行monitorenter指令時會嘗試獲取監視器的所有權,過程如下:
  • 若該監視器的進入次數為0,則該執行緒進入監視器並將進入次數設定為1,此時該執行緒即為該監視器的所有者
  • 若執行緒已經佔有該監視器並重入,則進入次數+1
  • 若其他執行緒已經佔有該監視器,則執行緒會被阻塞直到監視器的進入次數為0,之後執行緒間會競爭獲取該監視器的所有權
  • 只有首先獲得鎖的執行緒才能允許繼續獲取多個鎖

4.3.3 monitorexit指令

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
  • 執行monitorexit指令將遵循以下步驟:
  • 執行monitorexit指令的執行緒必須是物件例項所對應的監視器的所有者
  • 指令執行時,執行緒會先將進入次數-1,若-1之後進入次數變成0,則執行緒退出監視器(即釋放鎖)
  • 其他阻塞在該監視器的執行緒可以重新競爭該監視器的所有權

4.3.4 實現原理

  • 在同步程式碼塊中,JVM通過monitorenter和monitorexist指令實現同步鎖的獲取和釋放功能
  • monitorenter指令是在編譯後插入到同步程式碼塊的開始位置
  • monitorexit指令是插入到方法結束處和異常處
  • JVM要保證每個monitorenter必須有對應的monitorexit與之配對
  • 任何物件都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態
  • 執行緒執行monitorenter指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖
  • 執行緒執行monitorexit指令時,將會將進入次數-1直到變成0時釋放監視器
  • 同一時刻只有一個執行緒能夠成功,其它失敗的執行緒會被阻塞,並放入到同步佇列中,進入BLOCKED狀態

4.3.4 補充

  • 關於objectref可參見 鎖的使用方式
  • 由於 wait/notify 等方法底層實現是基於監視器,因此只有在同步方法(塊)中才能呼叫wait/notify等方法,否則會丟擲 java.lang.IllegalMonitorStateException 的異常的原因

4.4 同步方法同步原理

  • 區別於同步程式碼塊的監視器實現,同步方法通過使用 ACC_SYNCHRONIZED 標記符隱示的實現
  • 原理是通過方法呼叫指令檢查該方法在常量池中是否包含 ACC_SYNCHRONIZED 標記符,如果有,JVM 要求執行緒在呼叫之前請求鎖

5.進階原理

5.1 Monitor Obejct模式

5.1.1 Monitor Obejct模式綜述

  • Monitor其實是一種同步工具,也可以說是一種同步機制,它通常被描述為一個物件,主要特點是互斥和訊號機制
  • 互斥: 一個Monitor鎖在同一時刻只能被一個執行緒佔用,其他執行緒無法佔用
  • 訊號機制(signal): 佔用Monitor鎖失敗的執行緒會暫時放棄競爭並等待某個謂詞成真(條件變數),但該條件成立後,當前執行緒會通過釋放鎖通知正在等待這個條件變數的其他執行緒,讓其可以重新競爭鎖

Mesa派的signal機制

  • Mesa派的signal機制又稱"Non-Blocking condition variable"
  • 佔有Monitor鎖的執行緒發出釋放通知時,不會立即失去鎖,而是讓其他執行緒等待在佇列中,重新競爭鎖
  • 這種機制裡,等待者拿到鎖後不能確定在這個時間差裡是否有別的等待者進入過Monitor,因此不能保證謂詞一定為真,所以對條件的判斷必須使用while
  • Java中採用就是Mesa派的singal機制,即所謂的notify

5.1.2 Monitor Obejct模式結構

在 Monitor Object 模式中,主要有四種型別的參與者:

Java 8 併發篇 - 冷靜分析 Synchronized(下)

5.1.3 Monitor Obejct模式協作過程

1.同步方法的呼叫和序列化:

  • 當客戶執行緒呼叫監視者物件的同步方法時,必須首先獲取它的監視鎖
  • 只要該監視者物件有其他同步方法正在被執行,獲取操作便不會成功
  • 當監視者物件已被執行緒佔用時(即同步方法正被執行),客戶執行緒將被阻塞直到它獲取監視鎖
  • 當客戶執行緒成功獲取監視鎖後,進入臨界區,執行方法實現的服務
  • 一旦同步方法完成執行,監視鎖會被自動釋放,目的是使其他客戶執行緒有機會呼叫執行該監視者物件的同步方法

2.同步方法執行緒掛起:如果呼叫同步方法的客戶執行緒必須被阻塞或是有其他原因不能立刻進行,它能夠在一個監視條件(Monitor Condition)上等待,這將導致該客戶執行緒暫時釋放監視鎖,並被掛起在監視條件上

3.監視條件通知:一個客戶執行緒能夠通知一個監視條件,目的是通知阻塞在該監視條件(該監視鎖)的執行緒恢復執行

4.同步方法執行緒恢復:

  • 一旦一個早先被掛起在監視條件上的同步方法執行緒獲取通知,它將繼續在最初的等待監視條件的點上執行
  • 在被通知執行緒被允許恢復執行同步方法之前,監視鎖將自動被獲取(執行緒間自動相互競爭鎖)

對於Monitor筆者將在 ReentractLock 一文中進一步闡述

5.2 物件頭

5.2.1 JVM記憶體中的物件

  • 在JVM中,物件在記憶體中的佈局分成三塊區域:物件頭、示例資料和對齊填充
  • 物件頭: 物件頭主要儲存物件的hashCode、鎖資訊、型別指標、陣列長度(若是陣列的話)等資訊
  • 示例資料:存放類的屬性資料資訊,包括父類的屬性資訊,如果是陣列的例項部分還包括陣列長度,這部分記憶體按4位元組對齊
  • 填充資料:由於JVM要求物件起始地址必須是8位元組的整數倍,當不滿足8位元組時會自動填充(因此填充資料並不是必須的,僅僅是為了位元組對齊)

5.2.2 物件頭綜述

Java 8 併發篇 - 冷靜分析 Synchronized(下)
  • synchcronized的鎖是存放在Java物件頭中的
  • 如果物件是陣列型別,JVM用3個子寬(Word)儲存物件頭,否則是用2個子寬
  • 在32位虛擬機器中,1子寬等於4個位元組,即32bit;64位的話就是8個位元組,即64bit

5.2.3 Mark Word的儲存結構

32位JVM的Mark Word的預設儲存結構(無鎖狀態)

Java 8 併發篇 - 冷靜分析 Synchronized(下)

在執行期間,Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化(32位)

Java 8 併發篇 - 冷靜分析 Synchronized(下)

64位JVM的Mark Word的預設儲存結構(對於32位無鎖狀態,有25bit沒有使用)

Java 8 併發篇 - 冷靜分析 Synchronized(下)

5.3 Monitor Record

5.3.1 Monitor Record綜述

  • MonitorRecord(統一簡稱MR)是Java執行緒私有的資料結構,每一個執行緒都有一個可用MR列表,同時還有一個全域性的可用列表
  • 一個被鎖住的物件都會和一個MR關聯(物件頭的MarkWord中的LockWord指向MR的起始地址)
  • MR中有一個Owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用

5.3.2 Monitor Record結構

Java 8 併發篇 - 冷靜分析 Synchronized(下)

5.3.3 Monitor Record工作機理

Java 8 併發篇 - 冷靜分析 Synchronized(下)
  • 執行緒如果獲得監視鎖成功,將成為該監視鎖物件的擁有者
  • 在任一時刻,監視器物件只屬於一個活動執行緒(Owner)
  • 擁有者可以呼叫wait方法自動釋放監視鎖,進入等待狀態

6.鎖優化

6.1 自旋鎖

  • 痛點:由於執行緒的阻塞/喚醒需要CPU在使用者態和核心態間切換,頻繁的轉換對CPU負擔很重,進而對併發效能帶來很大的影響
  • 現象:通過大量分析發現,物件鎖的鎖狀態通常只會持續很短一段時間,沒必要頻繁地阻塞和喚醒執行緒
  • 原理:通過執行一段無意義的空迴圈讓執行緒等待一段時間,不會被立即掛起,看持有鎖的執行緒是否很快釋放鎖,如果鎖很快被釋放,那當前執行緒就有機會不用阻塞就能拿到鎖了,從而減少切換,提高效能
  • 隱患:若鎖能很快被釋放,那麼自旋效率就很好(真正執行的自旋次數越少效率越好,等待時間就少);但若是鎖被一直佔用,那自旋其實沒有做任何有意義的事但又白白佔用和浪費了CPU資源,反而造成資源浪費
  • 注意:自旋次數必須有個限度(或者說自旋時間),如果超過自旋次數(時間)還沒獲得鎖,就要被阻塞掛起
  • 使用: JDK1.6以上預設開啟-XX:+UseSpinning,自旋次數可通過-XX:PreBlockSpin調整,預設10次

6.2 自適應自旋鎖

  • 痛點:由於自旋鎖只能指定固定的自旋次數,但由於任務的差異,導致每次的最佳自旋次數有差異
  • 原理:通過引入"智慧學習"的概念,由前一次在同一個鎖上的自旋時間和鎖的持有者的狀態來決定自旋的次數,換句話說就是自旋的次數不是固定的,而是可以通過分析上次得出下次,更加智慧
  • 實現:若當前執行緒針對某鎖自旋成功,那下次自旋此時可能增加(因為JVM認為這次成功是下次成功的基礎),增加的話成功機率可能更大;反正,若自旋很少成功,那麼自旋次數會減少(減少空轉浪費)甚至直接省略自旋過程,直接阻塞(因為自旋完全沒有意義,還不如直接阻塞)
  • 補充:有了自適應自旋鎖,隨著程式執行和效能監控資訊的不斷完善,JVM對鎖的狀況預測會越來越準確,JVM會變得越來越智慧

6.3 阻塞鎖

6.3.1 阻塞鎖

  • 加鎖成功:當出現鎖競爭時,只有獲得鎖的執行緒能夠繼續執行
  • 加鎖失敗:競爭失敗的執行緒會由running狀態進入blocking狀態,並被放置到與目標鎖相關的一個等待佇列中
  • 解鎖:當持有鎖的執行緒退出臨界區,釋放鎖後,會將等待佇列中的一個阻塞執行緒喚醒,令其重新參與到鎖競爭中
  • 補充:本篇不會涉及到具體的JVM型號的分析,有興趣的讀者可以看看針對HotSopt JVM的分析 深入JVM鎖機制1-synchronized

6.3.2 公平鎖

公平鎖就是獲得鎖的順序按照先到先得的原則,從實現上說,要求當一個執行緒競爭某個物件鎖時,只要這個鎖的等待佇列非空,就必須把這個執行緒阻塞並塞入隊尾(插入隊尾一般通過一個CAS操作保持插入過程中沒有鎖釋放)

6.3.3 非公平鎖

相對的,非公平鎖場景下,每個執行緒都先要競爭鎖,在競爭失敗或當前已被加鎖的前提下才會被塞入等待佇列,在這種實現下,後到的執行緒有可能無需進入等待佇列直接競爭到鎖(隨機性)

6.4 鎖粗化

  • 痛點:多次連線在一起的加鎖、解鎖操作會造成
  • 原理:將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成一個範圍更大的鎖
  • 使用:將多個彼此靠近的同步塊合同在一個同步塊 或 把多個同步方法合併為一個方法
  • 補充:在JDK內建的API中,例如StringBuffer、Vector、HashTable都會存在隱性加鎖操作,可合併
/**
  * StringBuffer是執行緒安全的字串處理類
  * 每次呼叫stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機器檢測到有一系列連串的對同一個物件加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖
  */
StringBuffer stringBuffer = new StringBuffer();
public void append(){
    stringBuffer.append("kira");
    stringBuffer.append("sally");
    stringBuffer.append("mengmeng");
}
複製程式碼

6.5 鎖消除

  • 痛點:根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那麼可以認為這段程式碼是執行緒安全的,不必要加鎖
  • 原理: JVM在編譯時通過對執行上下文的描述,去除不可能存在共享資源競爭的鎖,通過這種方式消除無用鎖,即刪除不必要的加鎖操作,從而節省開銷
  • 使用: 逃逸分析和鎖消除分別可以使用引數-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(鎖消除必須在-server模式下)開啟
  • 補充:在JDK內建的API中,例如StringBuffer、Vector、HashTable都會存在隱性加鎖操作,可消除
/**
  * 比如執行10000次字串的拼接
  */
public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    for (int i = 0 ; i < 10000 ; i++){
        synchronizedDemo.append("kira","sally");
    }
}
public void append(String str1,String str2){
    //由於StringBuffer物件被封裝在方法內部,不可能存在共享資源競爭的情況
    //因此JVM會認為該加鎖是無意義的,會在編譯期就刪除相關的加鎖操作
    //還有一點特別要註明:明知道不會有執行緒安全問題,程式碼階段就應該使用StringBuilder
    //否則在沒有開啟鎖消除的情況下,StringBuffer不會被優化,效能可能只有StringBuilder的1/3
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str1).append(str2);
}/** 
複製程式碼

6.6 鎖的升級

  • 從JDK1.6開始,鎖一共有四種狀態:無鎖狀態偏向鎖狀態輕量鎖狀態重量鎖狀態
  • 鎖的狀態會隨著競爭情況逐漸升級,鎖允許升級但不允許降級
  • 不允許降級的目的是提高獲得鎖和釋放鎖的效率
  • 後面筆者會通過倒序的方式,即重量級鎖->輕量級鎖->偏向鎖進行講解,因為通常是前者的優化

鎖的升級過程

Java 8 併發篇 - 冷靜分析 Synchronized(下)

6.7 重量級鎖

  • 重量級鎖通過物件內部的monitor實現(見上文的Monitor Object模式)
  • monitor的本質是依賴於底層作業系統的MutexLock實現,作業系統實現執行緒間的切換是通過使用者態與核心態的切換完成的,而切換成本很高
  • MutexLock最核心的理念就是 嘗試獲取鎖.若可得到就佔有.若不能,就進入睡眠等待
  • 有興趣的讀者可以閱讀 淺談Mutex (Lock) ,該篇對Liunx的MutexLock做了很好的講解

6.8 輕量級鎖

6.8.1 輕量級鎖綜述

  • 痛點:由於執行緒的阻塞/喚醒需要CPU在使用者態和核心態間切換,頻繁的轉換對CPU負擔很重,進而對併發效能帶來很大的影響
  • 主要目的: 在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗
  • 升級時機: 當關閉偏向鎖功能或多執行緒競爭偏向鎖會導致偏向鎖升級為輕量級鎖
  • 原理: 在只有一個執行緒執行同步塊時進一步提高效能
  • 資料結構: 包括指向棧中鎖記錄的指標鎖標誌位
  • 補充:建議讀者先閱讀<<深入瞭解JVM虛擬機器>>的第8章虛擬機器位元組碼執行引擎的棧幀相關知識
Java 8 併發篇 - 冷靜分析 Synchronized(下)

6.8.2 輕量級鎖流程圖

執行緒1和執行緒2同時爭奪鎖,並導致鎖膨脹成重量級鎖

Java 8 併發篇 - 冷靜分析 Synchronized(下)

6.8.3 輕量級鎖加鎖

  • 1.執行緒在執行同步塊之前,JVM會先在當前執行緒的棧幀中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中(Displaced Mark Word-即被取代的Mark Word)做一份拷貝
  • 2.拷貝成功後,執行緒嘗試使用CAS將物件頭的Mark Word替換為指向鎖記錄的指標(將物件頭的Mark Word更新為指向鎖記錄的指標,並將鎖記錄裡的Owner指標指向Object Mark Word)
  • 如果更新成功,當前執行緒獲得鎖,繼續執行同步方法
  • 如果更新失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖,若自旋後沒有獲得鎖,此時輕量級鎖會升級為重量級鎖,當前執行緒會被阻塞

6.8.4 輕量級鎖解鎖

  • 解鎖時會使用CAS操作將Displaced Mark Word替換回到物件頭,
  • 如果解鎖成功,則表示沒有競爭發生
  • 如果解鎖失敗,表示當前鎖存在競爭,鎖會膨脹成重量級鎖,需要在釋放鎖的同時喚醒被阻塞的執行緒,之後執行緒間要根據重量級鎖規則重新競爭重量級鎖

6.8.5 輕量級鎖注意事項

  • 隱患:對於輕量級鎖有個使用前提是"沒有多執行緒競爭環境",一旦越過這個前提,除了互斥開銷外,還會增加額外的CAS操作的開銷,在多執行緒競爭環境下,輕量級鎖甚至比重量級鎖還要慢

6.9 偏向鎖

6.9.1 偏向鎖綜述

  • 痛點: Hotspot作者發現在大多數情況下不存在多執行緒競爭的情況,而是同一個執行緒多次獲取到同一個鎖,為了讓執行緒獲得鎖代價更低,因此設計了偏向鎖 (這個跟業務使用有很大關係)
  • 主要目的: 為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑
  • 原理: 在只有一個執行緒執行同步塊時通過增加標記檢查而減少CAS操作進一步提高效能
  • 資料結構: 包括佔有鎖的執行緒id是否是偏向鎖epoch(偏向鎖的時間戳)物件分代年齡鎖標誌位
Java 8 併發篇 - 冷靜分析 Synchronized(下)

6.9.2 偏向鎖流程圖

執行緒1演示了偏向鎖的初始化過程,執行緒2演示了偏向鎖的撤銷鎖過程

Java 8 併發篇 - 冷靜分析 Synchronized(下)

6.9.3 偏向鎖初始化

  • 當一個執行緒訪問同步塊並獲取到鎖時,會在物件頭和棧幀中的鎖記錄裡儲存偏向鎖的執行緒ID,以後該執行緒在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而是先簡單檢查物件頭的MarkWord中是否儲存了執行緒:
  • 如果已儲存,說明執行緒已經獲取到鎖,繼續執行任務即可
  • 如果未儲存,則需要再判斷當前鎖否是偏向鎖(即物件頭中偏向鎖的標識是否設定為1,鎖標識位為01):
  • 如果沒有設定,則使用CAS競爭鎖(說明此時並不是偏向鎖,一定是等級高於它的鎖)
  • 如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒,也就是結構中的執行緒ID

6.9.4 偏向鎖撤銷鎖

  • 偏向鎖使用一種等到競爭出現才釋放鎖的機制,只有當其他執行緒競爭鎖時,持有偏向鎖的執行緒才會釋放鎖
  • 偏向鎖的撤銷需要等待全域性安全點(該時間點上沒有位元組碼正在執行)
  • 偏向鎖的撤銷需要遵循以下步驟:

首先會暫停擁有偏向鎖的執行緒並檢查該執行緒是否存活

  1. 如果執行緒非活動狀態,則將物件頭設定為無鎖狀態(其他執行緒會重新獲取該偏向鎖)
  2. 如果執行緒是活動狀態,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄,並將對棧中的鎖記錄和物件頭的MarkWord進行重置
  • 要麼重新偏向於其他執行緒(即將偏向鎖交給其他執行緒,相當於當前執行緒"被"釋放了鎖)
  • 要麼恢復到無鎖或者標記鎖物件不適合作為偏向鎖(此時鎖會被升級為輕量級鎖)

最後喚醒暫停的執行緒,被阻塞在安全點的執行緒繼續往下執行同步程式碼塊

6.9.5 偏向鎖關閉鎖

  • 偏向鎖在JDK1.6以上預設開啟,開啟後程式啟動幾秒後才會被啟用
  • 有必要可以使用JVM引數來關閉延遲 -XX:BiasedLockingStartupDelay = 0
  • 如果確定鎖通常處於競爭狀態,則可通過JVM引數 -XX:-UseBiasedLocking=false 關閉偏向鎖,那麼預設會進入輕量級鎖

6.9.6 偏向鎖注意事項

  • 優勢:偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,其餘時刻不需要CAS指令(相比其他鎖)
  • 隱患:由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的CAS原子指令的效能消耗(這個通常只能通過大量壓測才可知)
  • 對比:輕量級鎖是為了線上程交替執行同步塊時提高效能,而偏向鎖則是在只有一個執行緒執行同步塊時進一步提高效能

6.10 偏向鎖 vs 輕量級鎖 vs 重量級鎖

Java 8 併發篇 - 冷靜分析 Synchronized(下)


歡迎關注知乎專欄《跟上Java8》,分享優秀的Java8中文指南、教程,同時歡迎投稿高質量的文章。


Synchronized一文通(1.8版) 黃志鵬kira 創作,採用 知識共享 署名-非商業性使用 4.0 國際 許可協議進行許可。


相關文章