Overview of Content
在現代計算機系統中,效能和安全性是至關重要的兩大指標。而在 Java 開發中,記憶體模型(Java Memory Model,JMM)和同步機制(synchronized)是實現這兩大指標的關鍵所在
這篇文章將帶你深入了解 Java 記憶體模型的規劃、CPU 高速緩存的運作原理,以及同步機制的底層實現與比較。無論您是初學者還是資深開發者,都能從中獲得有價值的知識,提升您的 Java 編程能力
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看
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 |
● 接著,我們假設 CPU 讀取內存 1M 的數據,那它會耗費多少時間呢?這裡我們試算看看…
/** * 假設有 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 內存中,減少從內存讀取的次數,提高處理效率
// linux 指令
watch -n 1 "lscpu"
Window 從工作管理員中就可以看到
● CPU 快速緩存模型(以下是 4 核 CPU),主要有分為 L1 L2 L3 快速緩存,L1、L2唯獨有緩存 L3 為共享緩存
越接近 CPU 的緩存所需要耗費的時間越少,但相對的設備成本就高
理解 Java 記憶體模型 JMM 的規劃
● 先大致了解 Java 記憶體模型,WorkThread 在操作主記憶體變量時都擁有一個變量複本,Thread 在改變這個複本後在寫回主記憶體
● JMM 定義了執行序 & 主記憶體之間的抽象關係,執行序之間的共享變量儲存在主記憶體中(Main Memory),而每個執行序都有一個 「私有」的本地記憶體(Local Memory),私有記憶體了該執行序已讀寫變量的副本
而執行序自身的 本地(私有)記憶體只是一個抽象 並不存在,它包含了緩存、暫存器、編譯優化... 等等訊息,概念如下
探索 synchronized 實現原理
● Synchronized 是 Java 內置同步鎖的機制,我們現在使用 Java 指令對其反編譯,以下是使用 Java 反編譯指令,可以觀察到使用 synchronized 時會使用到關鍵字 monitorenter、monitorexit
# Java 指令
javac MySync
javap -c MySyncTask.class # -c is 反組譯
A.
monitorenter
為同步開始的位子,monitorexit
為同步結束的位子,兩者是成對出現的B. 當成是執行到 monitorenter 會嘗試獲取到 Monitor 的所有權 & 嘗試獲取 synchronized 對象的鎖,monitorexit 則是在方法結束 or 異常處
// 以下都使用該程式
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
public synchronized void _SyncMethod() {
a = a + 1; //a++;
}
--反編譯內容--
反編譯:同步類物件
● 重點在於同步的是物件 this
,可以觀察到其 反編譯內容有可以看到 monitorenter
(3)、monitorexit
(21) 的範圍
public void _SyncThis() {
synchronized (this) {
a = a + 1; //a++;
}
}
--反編譯內容--
反編譯:同步類
● 重點在於同步的是 class 類,也可以看出 monitorenter
(4)、monitorexit
(22)的範圍
public void _SyncClass() {
synchronized (TestSynch.class) {
a = a + 1; //a++;
}
}
--反編譯內容--
.
物件頭:Synchronized 鎖的狀態
鎖的狀態都是 synchronized 內部對於物件頭中的鎖 的狀態鎖更改,它是 synchronized 關鍵字的一種優化(它會依照不同的狀況改變鎖的狀態,不同狀態又有不同的消能)
鎖的狀態:無鎖 -> 偏向鎖 -> 輕量級 -> 重量級,可以升級鎖的狀態,但是不能降級
無鎖就先不介紹 (它就是沒有鎖,所以物件頭也不會改變)
Java 物件、synchronized 物件鎖
● Java 物件在虛擬機中的內存,而 synchronized 物件鎖存在物件頭(Header
)中的 MarkWord
,以下畫出的為有鎖狀態的 MarkWord (紅箭頭),以及正常狀態無鎖狀態 (如果使用 synchronized 關鍵字,物件頭就會改變)
而有鎖狀態的差異如下,主要有分為「偏向鎖」、「輕量級鎖」、「重量級鎖」
認識 JVM 自旋鎖
● 自旋鎖是一種 機制、行為
我們知道執行序的休眠、喚醒是十分花費時間、性能的 (大概要花費 10000 ~ 20000 個指令時間,一個指令時間又花 0.6ns 再加上兩次就是兩倍的時間),如果小小的等後一下就可以進入鎖的話,那就自旋旋判斷一下即可
自旋鎖的實現之一就是 CAS 機制
● 自旋不斷判斷也是消費性能的,所以必須設定一個自旋上限,超過上限時就進入堵塞狀態(CPU 不再循環判斷),堵塞不會占用 CPU 時間,但是自旋則會 (這也是要注意的地方)
● 自旋時間過長則會非常消耗 CPU 性能,比起休眠更加消耗,所以要衡量 CAS 比較次數的上限
A. JDK 1.5 默認為 10 次
B. JDK 1.6 引入了適應性自旋鎖,使用 -XX+UseSpining 開啟自旋鎖
C. JDK 1.7 去除自旋鎖的設定參數,改由 JVM 自控制
偏向鎖、輕量級鎖
● 偏向鎖的定義:
大多數情況下不需要競爭,基本上都是由 A 執行序獲得鎖的所有權,減少不必要的 CAS 操作,因而引入偏向鎖
● 通常使用在第一個獲取鎖的執行序,當有其他的執行序進爭鎖 JVM 則會將該鎖升級成「輕量鎖」,而該 鎖升級過程是會觸發 stop the world
A. Thread-A 訪問對象,並確認對象頭的狀態,鎖的標誌是否為 1,Flag 是否為 01
B. 都確認完後就設置,並進入偏向鎖狀態,並執行步驟 5
C. Thread-B 確認,如果已經有 Thread ID,則使用 CAS 方式競爭鎖,競爭成功執行步驟 5,失敗執行步驟 4
D. Thread-B CAS 競爭失敗後,當達到了 safepoint 時獲得偏向鎖的執行序被掛起,並使用 stop the world
將鎖升級為輕量級鎖,之後被掛起的 Thread-B 執行同步區塊
E. 執行同步的區塊程式
● JVM 關閉/開啟 偏向鎖
開啟 :
-XX:+UseBiasedLocking
、-XX:BiasedLockingStartupDelay=0
關閉 :
-XX:-UseBiasedLocking
輕量級、重量級鎖
● 輕量級定義 : 當一執行序對象頭已經是 偏向鎖 時,還有一個執行序來競爭鎖這時就會升級為輕量級鎖
● 一個無狀態也可以直接經由 Lock record 跳過偏向鎖 (被 JVM 設定為不可偏向鎖),升級為輕量級鎖
A. 將無鎖狀態的 MarkWord 複製到當前執行序 Thread-A 的棧楨中 (標明為 Lock Record) 用於儲存對象目前的 MarkWord 拷貝,
B. JVM 使用 CAS 競爭中,成功的指向 Lock Record 區塊中的 object mark word,Thread-A 修改 MarkWord 到輕量級鎖 00,並執行同步區塊
C. Thread-B 競爭失敗,使用 CAS 機制自旋鎖
D. 當 Thread-B 使用自旋鎖嘗試次數高過限制時會將對象的鎖升級為重量級鎖 10,接著 Thread-B 進入執行序休眠狀態
E. Thread-A 執行完畢任務後釋放鎖,換成 Thread-B 執行
Synchronized 鎖的比較
● 針對 Synchronized 中鎖的比較,這些鎖是 Synchronized 的一種優化
Synchronized 鎖的差異 | 優點 | 缺點 | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖 & 解鎖不需要進入等待週期(上下文切換),也不必如 CAS 比較 | 如果多執行序會有鎖的撤銷(一些性能消耗) | 只有一個執行序訪問該同步區塊時 |
輕量級鎖 | 鎖的競爭不用上下文切換,提高響應速度(使用自旋功能) | 如果過度自旋反而會拉低 CPU 響應速度(CAS 要不斷判斷) | 追求響應時間,同步區塊程式也相對少時(不需等待其他響應時) |
重量級鎖 | 使用上下文切換,CPU 會直接跳過它的執行 (不消耗 CPU 效能) | 響應速度較慢,畢竟要喚醒 & 休眠 | 同步缺塊程式較多時,或是要等待其他響應 |
更多的 Java 語言相關文章
Java 語言深入
● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!