--- title: 'Java 多執行緒 - 深入了解 Synchronized 鎖' disqus: kyleAlien --- Java 多執行緒 - 深入了解 Synchronized 鎖 === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile: [TOC] :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**探索 Java 記憶體模型與同步機制的奧秘 | Synchronized 鎖的 3 種狀態**](https://devtechascendancy.com/jmm_comparison-of-synchronized-locks/) ::: ## JMM 計算機原理 JMM 的全名是 Java Memory Model,**其定義了 Java 虛擬機(JVM) 在計算機內存(PC Memory)中的工作方式**,所以 JMM 是屬於 JVM 底下的,Java 1.5 版本對它進行了重構,現在的 Java 仍繼續使用,**在 JMM 中遇到的問題跟現在的 PC 是差不多的** 由 Jeff Dean 在 Google 全體工程大會的報告,我們可以大致認識一下每個裝置處理數據的時間… 如下表所示 | Operation | Use Time | | -------- | -------- | | 打開一個站點 | 幾s | | 查看數據厙(有引索) | 10幾ms | | 1.6G CPU 執行一條指令 | 0.6ns | | ++機械式硬碟++ 讀取 1M 數據 | 2~10ms | | ++SSD 固態硬碟++ 讀取 1M 數據 | 0.3ms | | ++內存記憶體++ 讀取 1M 數據 | 250us | | CPU 讀取一次內存 | 100ns | | 1G 網卡傳輸 2KB 數據 | 20ms | :::info * 接著,我們假設 CPU 讀取內存 1M 的數據,那它會耗費多少時間呢?這裡我們試算看看… ```c= /** * 假設有 1M 數據 */ 1024 * 1024 = 1048576 (1MByte 多) /** * CPU 讀取 1M 數據,並且一次讀 4 Byte */ 1048576 / 4 = 262144 次的讀取動作 /** * 1.6G CPU 一條指令 0.6ns */ 262144 * 0.6ns = 0.157286ms (執行 1M 讀取) /** * 由於我們上面是計算一次讀取 4 Byte,那假設 CPU 處理 4Byte 需要 100ns */ 262144 * 100ns = 26214400 = 26.2144ms (執行 & 讀取) /** * CPU 讀取完 1M 所耗費的時間 */ 26.2144ms + 0.157286ms = 26.37ms ``` ::: ### CPU 高速緩存:多種不同的記憶體 * 從上面可以看出來 CPU 處理指令的時間是相當快的,但是**多工處理器是分開機算,==必須要內存互交==**,近期 cpu 處理速度越來越快,已經是好幾倍的超越了(0.3m/0.6n),所以**中間一定會插入一個接近 cpu 處理速度的緩存** * 快速緩存 cache 是比數據複製到 cpu 內存中,減少從內存讀取的次數,提高處理效率 ```shell= // linux 指令 watch -n 1 "lscpu" ``` > ![](https://i.imgur.com/GSFJjgY.png) Window 從工作管理員中就可以看到 > ![](https://i.imgur.com/RmCyDgL.png) * CPU 快速緩存模型(以下是 4 核 CPU),主要有分為 L1 L2 L3 快速緩存,L1、L2唯獨有緩存 L3 為共享緩存 > ![](https://i.imgur.com/1Jye5Dy.png) **越接近 CPU 的緩存所需要耗費的時間越少,但相對的設備成本就高** > ![](https://i.imgur.com/j513apD.png) ### 理解 Java 記憶體模型 JMM 的規劃 * 先大致了解 Java 記憶體模型,**WorkThread 在操作主記憶體變量時都擁有一個變量複本**,Thread 在改變這個複本後在寫回主記憶體 > ![](https://i.imgur.com/5th5ovE.png) * JMM 定義了執行序 & 主記憶體之間的抽象關係,**執行序之間的共享變量儲存在主記憶體中(Main Memory)**,而**每個執行序都有一個 ==「私有」的本地記憶體(Local Memory)==**,私有記憶體了該執行序已讀寫變量的副本 而執行序自身的 **++本地記憶體只是一個抽象++ 並不存在**,它包含了緩存、暫存器、編譯優化... 等等訊息,概念如下 > ![](https://i.imgur.com/nKvBe0i.png) ## 探索 synchronized 實現原理 * Synchronized 是 Java 內置同步鎖的機制,我們現在使用 Java 指令對其反編譯,以下是使用 Java 反編譯指令,**可以觀察到使用 synchronized 時會使用到關鍵字 ==monitorenter==、==monitorexit==** ```shell= # Java 指令 javac MySync javap -c MySyncTask.class # -c is 反組譯 ``` :::info 1. **`monitorenter` 為同步開始的位子,`monitorexit` 為同步結束的位子,兩者是成對出現的** 2. 當成是執行到 monitorenter 會嘗試獲取到 Monitor 的所有權 & 嘗試獲取 synchronized 對象的鎖,monitorexit 則是在方法結束 or 異常處 ::: ```java= // 以下都使用該程式 public class TestSynch { private int a = 0; public synchronized void _SyncMethod() { a = a + 1; //a++; } public void _SyncThis() { synchronized (this) { a = a + 1; //a++; } } public void _SyncClass() { synchronized (TestSynch.class) { a = a + 1; //a++; } } } ``` ### 反編譯:同步方法 * **同步方法在反組譯時是看不出有 monitor 關鍵字的,但是==可以發現 `ACC_SYNCHRONIZED`== 關鍵字,而它會呼叫 `monitor`** ```java= public synchronized void _SyncMethod() { a = a + 1; //a++; } ``` **--反編譯內容--** > ![](https://i.imgur.com/jLsoEaY.png) ### 反編譯:同步類物件 * 重點在於同步的是物件 `this`,可以觀察到其 **反編譯內容有可以看到 `monitorenter`(3)、`monitorexit`(21) 的範圍** ```java= public void _SyncThis() { synchronized (this) { a = a + 1; //a++; } } ``` **--反編譯內容--** > ![](https://i.imgur.com/SSkmLEJ.png) ### 反編譯:同步類 * 重點在於同步的是 class 類,也**可以看出 `monitorenter`(4)、`monitorexit`(22)的範圍** ```java= public void _SyncClass() { synchronized (TestSynch.class) { a = a + 1; //a++; } } ``` **--反編譯內容--** > ![](https://i.imgur.com/5NsEFbr.png) ## 物件頭:Synchronized 鎖的狀態 **鎖的狀態都是 ++synchronized 內部對於物件頭中的鎖++ 的狀態鎖更改,它是 synchronized 關鍵字的一種優化**(它會依照不同的狀況改變鎖的狀態,不同狀態又有不同的消能) 鎖的狀態:**無鎖 -> 偏向鎖 -> 輕量級 -> 重量級**,**可以升級鎖的狀態,但是不能降級** > 無鎖就先不介紹 (它就是沒有鎖,所以物件頭也不會改變) ```mermaid graph LR 無鎖 --> 偏向鎖 --> 輕量級 --> 重量級 ``` ### Java 物件、synchronized 物件鎖 * [**Java 物件在虛擬機中的內存**](https://hackmd.io/kKOqFUXpRhyYTchh98xUUQ?view#%E5%B0%8D%E8%B1%A1%E7%9A%84%E5%85%A7%E5%AD%98),而 **synchronized 物件鎖存在物件頭(`Header`)中的 `MarkWord`**,以下畫出的為有鎖狀態的 MarkWord (紅箭頭),以及正常狀態無鎖狀態 (如果使用 synchronized 關鍵字,物件頭就會改變) > ![](https://i.imgur.com/KYoXZ6a.png) 而有鎖狀態的差異如下,**主要有分為「偏向鎖」、「輕量級鎖」、「重量級鎖」** > ![](https://i.imgur.com/yQLrEKU.png) ### 認識 JVM 自旋鎖 * **自旋鎖是一種 ++機制、行為++** 我們知道執行序的休眠、喚醒是十分花費時間、性能的 (大概要花費 10000 ~ 20000 個指令時間,一個指令時間又花 0.6ns 再加上兩次就是兩倍的時間),如果小小的等後一下就可以進入鎖的話,那就自旋旋判斷一下即可 **自旋鎖的實現之一就是 CAS 機制** * **自旋不斷判斷也是消費性能的,所以必須設定一個自旋上限**,超過上限時就進入堵塞狀態(CPU 不再循環判斷),**==堵塞不會占用 CPU 時間,但是自旋則會== (這也是要注意的地方)** :::warning * **自旋時間過長則會非常消耗 CPU 性能,比起休眠更加消耗**,所以要衡量 CAS 比較次數的上限 1. JDK 1.5 默認為 10 次 2. JDK 1.6 引入了適應性自旋鎖,使用 -XX+UseSpining 開啟自旋鎖 3. JDK 1.7 去除自旋鎖的設定參數,改由 JVM 自控制 ::: ### 偏向鎖、輕量級鎖 * **偏向鎖的定義**: 大多數情況下不需要競爭,基本上都是由 A 執行序獲得鎖的所有權,減少不必要的 CAS 操作,因而引入偏向鎖 * 通常使用在第一個獲取鎖的執行序,當有其他的執行序進爭鎖 JVM 則會將該鎖升級成「**輕量鎖**」,而該 **鎖升級過程是會觸發 `stop the world`** 1. Thread-A 訪問對象,並確認對象頭的狀態,鎖的標誌是否為 1,Flag 是否為 01 2. 都確認完後就設置,並進入偏向鎖狀態,並執行步驟 5 3. Thread-B 確認,如果已經有 Thread ID,則使用 CAS 方式競爭鎖,競爭成功執行步驟 5,失敗執行步驟 4 4. Thread-B CAS 競爭失敗後,當達到了 safepoint 時獲得偏向鎖的執行序被掛起,並使用 `stop the world` 將鎖升級為輕量級鎖,之後被掛起的 Thread-B 執行同步區塊 5. 執行同步的區塊程式 > ![](https://i.imgur.com/b6sP8Rv.png) :::info * **JVM 關閉/開啟 偏向鎖** **開啟** : `-XX:+UseBiasedLocking`、`-XX:BiasedLockingStartupDelay=0` **關閉** : `-XX:-UseBiasedLocking` ::: ### 輕量級、重量級鎖 * **輕量級定義** : 當一執行序對象頭已經是 **偏向鎖** 時,**還有一個執行序來競爭鎖這時就會升級為輕量級鎖** * 一個無狀態也可以直接經由 Lock record 跳過偏向鎖 (被 JVM 設定為不可偏向鎖),升級為輕量級鎖 1. 將無鎖狀態的 MarkWord 複製到當前執行序 Thread-A 的棧楨中 (標明為 Lock Record) 用於儲存對象目前的 MarkWord 拷貝, 2. JVM 使用 CAS 競爭中,成功的指向 Lock Record 區塊中的 object mark word,Thread-A 修改 MarkWord 到輕量級鎖 00,並執行同步區塊 3. Thread-B 競爭失敗,使用 CAS 機制自旋鎖 4. 當 Thread-B 使用自旋鎖嘗試次數高過限制時會將對象的鎖升級為重量級鎖 10,接著 Thread-B 進入執行序休眠狀態 5. Thread-A 執行完畢任務後釋放鎖,換成 Thread-B 執行 > ![](https://i.imgur.com/PHBneIg.png) ### Synchronized 鎖的比較 * 針對 Synchronized 中鎖的比較,**這些鎖是 Synchronized 的一種優化** | 鎖的類別 | 優點 | 缺點 | 使用場景 | | -------- | -------- | -------- | - | | 偏向鎖 | 加鎖 & 解鎖不需要進入等待週期(上下文切換),也不必如 CAS 比較 | 如果多執行序會有鎖的撤銷(一些性能消耗) | 只有一個執行序訪問該同步區塊時 | | 輕量級鎖 | 鎖的競爭不用上下文切換,提高響應速度(使用自旋功能) | 如果過度自旋反而會拉低 CPU 響應速度(CAS 要不斷判斷) | 追求響應時間,同步區塊程式也相對少時(不需等待其他響應時) | | 重量級鎖 | 使用上下文切換,CPU 會直接跳過它的執行 (不消耗 CPU 效能) | 響應速度較慢,畢竟要喚醒 & 休眠 | 同步缺塊程式較多時,或是要等待其他響應 | ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎進階`