首頁 > 軟體

Java synchronized輕量級鎖的核心原理詳解

2022-03-01 19:00:32

問題:

什麼是自旋鎖?

說一下 synchronized 底層實現原理?

多執行緒中 synchronized 鎖升級的原理是什麼?

1. 輕量級鎖的原理

引入輕量級鎖的主要目的是在多執行緒競爭不激烈的情況下,通過CAS機制競爭鎖減少重量級鎖產生的效能損耗。重量級鎖使用了作業系統底層的互斥鎖(Mutex Lock),會導致執行緒在使用者態和核心態之間頻繁切換,從而帶來較大的效能損耗。

輕量級鎖的使用場景:如果一個物件雖然有多執行緒要加鎖,但加鎖的時間是錯開的(也就是沒有競爭),那麼可以 使用輕量級鎖來優化。

輕量鎖存在的目的是儘可能不動用作業系統層面的互斥鎖,因為其效能比較差。執行緒的阻塞和喚醒需要CPU從使用者態轉為核心態,頻繁地阻塞和喚醒對CPU來說是一件負擔很重的工作。同時我們可以發現,很多物件鎖的鎖定狀態只會持續很短的一段時間,例如整數的自加操作,在很短的時間內阻塞並喚醒執行緒顯然不值得,為此引入了輕量級鎖。輕量級鎖是一種自旋鎖,因為JVM本身就是一個應用,所以希望在應用層面上通過自旋解決執行緒同步問題。

public class Main {
    static final Object obj = new Object();
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
           method1();
        });
        thread.start();
    }

    public static void method1() {
        synchronized( obj ) {
            // 同步塊 A
            method2();
        }
    }
    public static void method2() {
        synchronized( obj ) {
            // 同步塊 B
        }
    }
}

輕量級鎖的執行過程:

在搶鎖執行緒進入臨界區之前,如果內建鎖沒有被鎖定,JVM首先將在搶鎖執行緒的棧幀中建立一個鎖記錄(Lock Record),用於儲存物件Mark Word的拷貝,

然後搶鎖執行緒將使用CAS自旋操作,嘗試將內建鎖物件頭的Mark Wordptr_to_lock_record(鎖記錄指標)更新為搶鎖執行緒棧幀中鎖記錄的地址,如果這個更新執行成功了,這個執行緒就擁有了這個物件鎖。然後JVM將Mark Word中的lock標記位改為00(輕量級鎖標誌),即表示該物件處於輕量級鎖狀態。搶鎖成功之後,JVM會將Mark Word中原來的鎖物件資訊(如雜湊碼等)儲存在搶鎖執行緒鎖記錄的Displaced Mark Word(可以理解為放錯地方的Mark Word)欄位中,再將搶鎖執行緒中鎖記錄的owner指標指向鎖物件。

鎖記錄是執行緒私有的,每個執行緒都有自己的一份鎖記錄,在建立完鎖記錄後,會將內建鎖物件的Mark Word複製到鎖記錄的Displaced Mark Word欄位。這是為什麼呢?因為內建鎖物件的MarkWord的結構會有所變化,Mark Word將會出現一個指向鎖記錄的指標,而不再存著無鎖狀態下的鎖物件雜湊碼等資訊,所以必須將這些資訊暫存起來,供後面在鎖釋放時使用。

(1) 在搶鎖執行緒進入臨界區之前,如果內建鎖沒有被鎖定,JVM首先將在搶鎖執行緒的棧幀中建立一個鎖記錄(Lock Record),每個執行緒都的棧幀都會包含一個鎖記錄的結構,內部可以儲存鎖定物件的mark word

(2) 搶鎖執行緒將使用CAS自旋操作,嘗試將內建鎖物件頭的mark word的ptr_to_lock_record(鎖記錄指標)更新為搶鎖執行緒棧幀中鎖記錄的地址,如果這個更新執行成功了,這個執行緒就擁有了這個物件鎖。然後jvm將mark word中的lock標記位改為00,即表示該物件處於輕量級鎖狀態。

搶鎖成功之後,jvm會將mark word中原來的鎖物件資訊(如雜湊碼等)儲存在搶鎖執行緒鎖記錄的Displaced Mark Word欄位中,再將搶鎖執行緒中鎖記錄的owner指標指向鎖物件。

64位元的mark word結構如表所示:

在輕量級鎖搶佔成功之後,鎖記錄和物件頭的狀態如圖所示:

鎖記錄是執行緒私有的,每個執行緒都有自己的一份鎖記錄,在建立完鎖記錄後,會將內建鎖物件的Mark Word複製到鎖記錄的Displaced Mark Word欄位。這是為什麼呢?因為內建鎖物件的mark word的結構會有所變化,mark word將會出現一個指向鎖記錄的指標,而不再存著無鎖狀態下的鎖物件雜湊碼等資訊,所以必須將這些資訊暫存起來,供後面在鎖釋放時使用。

(3) 如果 cas 失敗,有兩種情況:

  • 如果是其它執行緒已經持有了該 Object 的輕量級鎖,這時表明有競爭,進入鎖膨脹過程 ;
  • 如果是自己執行了 synchronized 鎖重入,那麼再新增一條 Lock Record 作為重入的計數;

(4) 當退出 synchronized 程式碼塊(解鎖時)如果有取值為 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減一

(5) 當退出 synchronized 程式碼塊(解鎖時)鎖記錄的值不為 null,這時使用 cas 將 mark word的值恢復給物件頭

成功,則解鎖成功失敗,說明輕量級鎖進行了鎖膨脹或已經升級為重量級鎖,進入重量級鎖解鎖流程

2. 輕量級鎖的分類

輕量級鎖主要有兩種:普通自旋鎖和自適應自旋鎖。

1、普通自旋鎖

所謂普通自旋鎖,就是指當有執行緒來競爭鎖時,搶鎖執行緒會在原地迴圈等待,而不是被阻塞,直到那個佔有鎖的執行緒釋放鎖之後,這個搶鎖執行緒才可以獲得鎖。

說明:

鎖在原地迴圈等待的時候是會消耗CPU的,就相當於在執行一個什麼也不幹的空迴圈。所以輕量級鎖適用於臨界區程式碼耗時很短的場景,這樣執行緒在原地等待很短的時間就能夠獲得鎖了。預設情況下,自旋的次數為10次,使用者可以通過-XX:PreBlockSpin選項來進行更改。

2、自適應自旋鎖

所謂自適應自旋鎖,就是等待執行緒空迴圈的自旋次數並非是固定的,而是會動態地根據實際情況來改變自旋等待的次數,自旋次數由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。自適應自旋鎖的大概原理是:

  • 如果搶鎖執行緒在同一個鎖物件上之前成功獲得過鎖,jvm就會認為這次自旋很有可能再次成功,因此允許自旋等待持續相對更長的時間。
  • 如果對於某個鎖,搶鎖執行緒很少成功獲得過,那麼jvm將可能減少自旋時間甚至省略自旋過程,以避免浪費處理器資源。

自適應自旋解決的是“鎖競爭時間不確定”的問題。自適應自旋假定不同執行緒持有同一個鎖物件的時間基本相當,競爭程度趨於穩定。總的思想是:根據上一次自旋的時間與結果調整下一次自旋的時間。

JDK 1.6的輕量級鎖使用的是普通自旋鎖,且需要使用-XX:+UseSpinning選項手工開啟。

JDK 1.7後,輕量級鎖使用自適應自旋鎖,JVM啟動時自動開啟,且自旋時間由JVM自動控制。

輕量級鎖也被稱為非阻塞同步、樂觀鎖,因為這個過程並沒有把執行緒阻塞掛起,而是讓執行緒空迴圈等待。

3. 輕量級鎖的膨脹

輕量級鎖的問題在哪裡呢?

雖然大部分臨界區程式碼的執行時間都是很短的,但是也會存在執行得很慢的臨界區程式碼。臨界區程式碼執行耗時較長,在其執行期間,其他執行緒都在原地自旋等待,會空消耗CPU。因此,如果競爭這個同步鎖的執行緒很多,就會有多個執行緒在原地等待繼續空迴圈消耗CPU(空自旋),這會帶來很大的效能損耗。

輕量級鎖的本意是為了減少多執行緒進入作業系統底層的互斥鎖的概率,並不是要替代作業系統互斥鎖。所以,在爭用激烈的場景下,輕量級鎖會膨脹為基於作業系統核心互斥鎖實現的重量級鎖。

如果在嘗試加輕量級鎖的過程中,CAS 操作無法成功,這時一種情況就是有其它執行緒為此物件加上了輕量級鎖(有 競爭),這時需要進行鎖膨脹,將輕量級鎖變為重量級鎖。

(1) 當 Thread-1 進行輕量級加鎖時,Thread-0 已經對該物件加了輕量級鎖

這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程,即為鎖物件申請 Monitor 鎖,讓鎖物件指向重量級鎖地址,然後自己進入 Monitor 的 EntryList BLOCKED

當 Thread-0 退出同步塊解鎖時,使用 cas 將mark word的值恢復給物件頭,失敗。這時會進入重量級解鎖 流程,即按照 Monitor 地址找到 Monitor 物件,設定 Owner 為 null,喚醒 EntryList 中 BLOCKED 執行緒。

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容! 


IT145.com E-mail:sddin#qq.com