1. 程式人生 > >自頂向下徹底理解 Java 中的 Synchronized

自頂向下徹底理解 Java 中的 Synchronized

閱讀本文至少要知道 synchronized 用來是幹什麼的... 需要的前置知識還有 Java 物件頭和 Java 位元組碼的部分知識。

synchronized 的使用

synchronized 有三種使用方式,三種方式鎖住的物件是不相同的。

鎖分為例項物件鎖class 物件鎖類物件鎖,注意這三種鎖是不一樣的。

  • 修飾例項方法,此時鎖住的是物件,鎖分為例項物件鎖
  • 修飾靜態方法,此時鎖住的是類物件鎖
  • 修飾程式碼段,此時鎖住的是括號中的物件(synchronized(this)),可以是例項物件鎖或者 class 物件鎖(synchronized(Object.class)

此時出現了鎖住類和鎖住物件,要注意這兩個鎖是不同的,在一個執行緒拿到類的鎖時,另外一個執行緒是可以拿到物件的鎖的。

synchronized 底層語義實現

程式碼同步塊和方法級別的synchronized使用在JVM 層實現是不一樣的。當然如果從 JVM 到 CPU 層面是採用 Lock 指令實現的。

每個物件都存在著一個 monitor 與之關聯,物件與其 monitor 之間的關係有存在多種實現方式,如monitor可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個 monitor 被某個執行緒持有後,它便處於鎖定狀態。

當多個執行緒同時請求某個物件監視器時,新請求鎖的執行緒將首先被加入到ConetentionList中。物件監視器會設定幾種狀態用來區分請求的執行緒:

Contention List:所有請求鎖的執行緒將被首先放置到該競爭佇列

Entry List:Contention List中那些有資格成為候選人的執行緒被移到Entry List

Wait Set:那些呼叫wait方法被阻塞的執行緒被放置到Wait Set

OnDeck:任何時刻最多隻能有一個執行緒正在競爭鎖,該執行緒稱為OnDeck

Owner:獲得鎖的執行緒稱為Owner

!Owner:釋放鎖的執行緒

synchrionized 位元組碼層在同步塊的入口插入 monitorenter,在同步塊出口插入monitorexit

方法級別的同步是通過在方法的 flag 表示上設定 ACC_SYNCHRONIZED 來實現的。具體可以檢視《深入理解 JVM 虛擬機器》 中位元組碼一章。

對 synchronized 的優化

JDK 1.6 實現了對鎖的大量優化。可以分為兩種,一種是減少對 synchronized 的使用,一種是在特殊條件下使用更輕量級的鎖來代替 synchronized。

減少對鎖的使用

鎖消除

當編譯器檢測到一些被加上 synchronized 的程式碼不存在競爭的時候(通過逃逸分析,感興趣可以去看一下《深入理解 Java 虛擬機器》),就會被視為執行緒私有的,鎖會被安全的消除掉。

鎖粗化

當編譯器發現 synchronized 被加入在迴圈當中,不斷的加鎖解鎖會有極大的效率問題。不要認為你不會寫出這麼傻的程式碼,JDK 中有許多方法是同步的,比如 HashTable 中的一些方法。

for (int i = 0; i < 100; i++) {
    synchronized (this) {
        //do something
    }
}

編譯器會自動把它優化成

synchronized (this) {
    for (int i = 0; i < 100; i++) {
        //do something
    }
}

來減少鎖的獲取和釋放。

自旋鎖與自適應鎖

有相當多一段程式碼在程式碼同步塊中只執行一小會兒,如果為了等待這一會兒去掛起和恢復執行緒,切換執行緒帶來的開銷不是很值得,在引入了自旋鎖後,當遇到鎖被別的執行緒佔用的時候,這個執行緒就進入一段忙迴圈,這就是自旋。

但是如果多次忙迴圈後仍然獲取不到鎖,那麼只能掛起執行緒將鎖升級為重量級鎖了。

自適應鎖會記錄之前在程式碼同步快的執行時間來決定是否要執行自旋以及自旋的時間,如果之前自旋成功過,那麼這次也很有可能會自旋成功。如果之前自旋失敗,那麼就省略掉自旋過程直接掛起執行緒避免浪費 CPU 資源。

通過輕量級鎖來代替 synchronized

輕量級鎖設計出來是想要在競爭較少的情況下減少 synchronized 的效能消耗,而不是用來代替 synchronized 的。想要看懂輕量級鎖的使用需要對 Java 物件頭有一定的瞭解。關於 Java 物件頭可以參考。好,接下來我就預設認為你懂 Mark Word 是什麼了。

鎖的膨脹過程是 偏向鎖→輕量級鎖→重量級鎖,膨脹過程的單方向的。不能縮小回來。

下面是 Mark Word 的內容和鎖的關係。

儲存內容 標誌位 狀態
物件雜湊碼,物件分代年齡 01 未鎖定
指向記錄鎖指標 00 輕量級鎖定
指向重量級鎖指標 10 膨脹(重量級鎖定)
11 GC 標記
偏向執行緒 id,時間戳,分代年齡 01 可偏向

偏向鎖

偏向鎖的思想就是:鎖經常被同一個執行緒重複獲取,那麼可以通過設定偏向鎖來避免使用重量級鎖。因為如果這段時間只有這一個執行緒在重複獲取這個物件的鎖,那麼對這部分程式碼的同步就是無意義的。

當執行緒獲取鎖的時候發現 Mark Word 是未鎖定的狀態,那麼就採用 CAS 把這個 Mark Word 設定成偏向狀態,把這個執行緒的 id 設定進去,然後如果這個執行緒再次獲取這個鎖的時候發現這個偏向鎖的 id 和當前執行緒的 id 一樣則不需要同步直接執行。

當有另外一個執行緒嘗試獲取這個偏向鎖的時候,鎖會恢復到未鎖定或者輕量級鎖的狀態。

  • 如果物件未被鎖定,則會變成未鎖定的,不可偏向的物件
  • 如果物件被鎖定了,則會變成輕量級鎖狀態

如果大多數鎖總是被多個不同的執行緒訪問,那麼偏向模式就是多餘的,可以 採用 --XX:UseBiaseLocking 來禁止偏向鎖來提高效能。

輕量級鎖

當執行緒進入一個程式碼同步塊的時候,虛擬機器將使用 CAS 將 Mark Word 更新為指向 Lock Record 的指標。如果成功則執行緒擁有這個物件鎖,mark word 將被設為 00。

如果更新失敗,則檢查該執行緒是否持有這個物件鎖,如果已經持有則直接向下執行

如果沒有持有這個物件鎖則輕量級鎖膨脹為重量級鎖,鎖標誌狀態變為 10。

參考文獻