【Synchronized】不同的使用場景和案例
- 【一】鎖的作用范圍與鎖對象
- 【1】實例方法(對象鎖)
- 【2】靜態方法(類鎖)
- 【3】代碼塊(顯式指定鎖對象)
- 【4】類鎖(通過Class對象顯式鎖定)
- 【二】核心區別總結
- 【三】關鍵注意事項
- 【1】內存可見性
- 【2】避免死鎖
- 【3】鎖粒度過大問題
- 【四】測試案例補充
- 【五】原理
- 【1】Synchronized鎖原理
- (1)對象頭
- (2)監視器(Monitor)
- (3)字節碼層面的實現
- 【2】鎖升級原理
- (1)無鎖狀態
- (2)偏向鎖
- (3)輕量級鎖
- (4)重量級鎖
- 【3】總結
【一】鎖的作用范圍與鎖對象
【1】實例方法(對象鎖)
(1)鎖對象:當前實例(this)。
(2)作用范圍:同一實例的多個線程訪問同步方法時互斥;不同實例的同步方法互不影響。
(3)測試案例:
public class BankAccount {private int balance = 1000;public synchronized void withdraw(int amount) {if (balance >= amount) {balance -= amount;}}
}// 測試代碼
BankAccount account1 = new BankAccount();
BankAccount account2 = new BankAccount();// 線程1操作account1的withdraw方法(互斥)
new Thread(() -> account1.withdraw(500)).start();
// 線程2操作account1的withdraw方法(被阻塞)
new Thread(() -> account1.withdraw(500)).start();
// 線程3操作account2的withdraw方法(不受影響)
new Thread(() -> account2.withdraw(500)).start();
(4)結果:線程1和線程2對account1的操作互斥;線程3與account1的操作無關
【2】靜態方法(類鎖)
(1)鎖對象:類的Class對象(如BankAccount.class)。
(2)作用范圍:所有實例調用靜態同步方法時互斥。
(3)測試案例:
public class OrderService {private static int orderCount = 0;public static synchronized void generateOrder() {orderCount++;}
}// 測試代碼
OrderService instanceA = new OrderService();
OrderService instanceB = new OrderService();// 線程1調用靜態方法(互斥)
new Thread(() -> OrderService.generateOrder()).start();
// 線程2調用靜態方法(被阻塞)
new Thread(() -> OrderService.generateOrder()).start();
// 線程3通過實例調用靜態方法(同樣被阻塞)
new Thread(() -> instanceA.generateOrder()).start();
(4)結果:所有線程調用generateOrder()都會競爭同一把類鎖,無論通過類名還是實例調用。
【3】代碼塊(顯式指定鎖對象)
(1)鎖對象:任意顯式指定的對象(如實例變量、類對象)。
(2)作用范圍:僅同步代碼塊內的操作,鎖粒度更細。
(3)測試案例:
public class CacheManager {private final Object lock = new Object();private int cacheSize = 0;public void updateCache() {// 非同步代碼System.out.println("非同步操作");synchronized(lock) { // 顯式鎖定lock對象cacheSize++;}}
}// 測試代碼
CacheManager manager = new CacheManager();
// 線程1和線程2競爭lock對象的鎖
new Thread(() -> manager.updateCache()).start();
new Thread(() -> manager.updateCache()).start();
(4)結果:僅代碼塊內的cacheSize++操作互斥,其他代碼可并發執行347。
【4】類鎖(通過Class對象顯式鎖定)
(1)鎖對象:類的Class對象(如ClassName.class)。
(2)作用范圍:與靜態方法鎖效果一致,但可用于非靜態方法中。
(3)測試案例:
public class LockDemo {public void classLockBlock() {synchronized(LockDemo.class) { // 顯式鎖定類對象// 同步代碼}}
}// 線程1和線程2調用classLockBlock方法時互斥
LockDemo obj1 = new LockDemo();
LockDemo obj2 = new LockDemo();
new Thread(() -> obj1.classLockBlock()).start();
new Thread(() -> obj2.classLockBlock()).start();
(4)結果:不同實例調用classLockBlock時仍互斥,因為鎖的是LockDemo.class
【二】核心區別總結
鎖類型 鎖對象 作用范圍 適用場景
實例方法(對象鎖) 當前實例(this) 同一實例的同步方法 保護實例變量(如賬戶余額)
靜態方法(類鎖) 類的Class對象 所有實例的靜態方法 保護靜態變量(如全局計數器)
顯式對象鎖 指定對象(如lock) 鎖定指定對象的同步代碼塊 細粒度控制(如緩存更新)
顯式類鎖 ClassName.class 跨實例同步(與靜態方法鎖等效) 需要全局同步的非靜態方法邏輯
【三】關鍵注意事項
【1】內存可見性
實例鎖僅保證同一實例內的變量可見性。
類鎖保證所有實例間的變量可見性(因Class對象在方法區共享)。
【2】避免死鎖
按固定順序獲取鎖(如先鎖A再鎖B)。
避免嵌套鎖(如synchronized方法內調用另一個synchronized方法時需謹慎)。
【3】鎖粒度過大問題
錯誤示例:將耗時操作(如IO)放在同步方法內。
優化方案:僅同步關鍵代碼塊
【四】測試案例補充
場景:對象鎖與類鎖的互斥性測試
public class MixedLockDemo {public synchronized void instanceMethod() {// 實例鎖}public static synchronized void staticMethod() {// 類鎖}
}// 線程1調用實例方法,線程2調用靜態方法
MixedLockDemo obj = new MixedLockDemo();
new Thread(() -> obj.instanceMethod()).start(); // 鎖obj實例
new Thread(() -> MixedLockDemo.staticMethod()).start(); // 鎖MixedLockDemo.class
結果:兩個線程不會互斥,因為實例鎖和類鎖是獨立的
【五】原理
【1】Synchronized鎖原理
(1)對象頭
在 Java 中,每個對象都有一個對象頭(Object Header),對象頭中包含了一些與對象自身相關的信息,如哈希碼、分代年齡等,同時也包含了鎖的相關信息。對象頭的結構在不同的 JVM 實現中可能會有所不同,但通常會包含一個 Mark Word 字段,這個字段用于存儲對象的鎖狀態、哈希碼等信息。
(2)監視器(Monitor)
synchronized 關鍵字的底層實現依賴于監視器(Monitor)。監視器是 Java 中實現同步的基礎,它是一個同步工具,相當于一個特殊的房間,線程進入這個房間就相當于獲得了鎖,其他線程則需要等待。每個 Java 對象都可以關聯一個監視器,當一個線程試圖訪問被 synchronized 修飾的代碼塊或方法時,它會首先嘗試獲取該對象的監視器。
(3)字節碼層面的實現
當使用 synchronized 修飾代碼塊時,編譯后的字節碼會包含 monitorenter 和 monitorexit 指令。例如:
public class SynchronizedExample {public void test() {synchronized (this) {// 同步代碼塊}}
}
編譯后的字節碼中會包含如下指令:
monitorenter
// 同步代碼塊的字節碼
monitorexit
(1)monitorenter 指令
當線程執行到 monitorenter 指令時,它會嘗試獲取對象的監視器。如果監視器的進入計數器為 0,表示該監視器沒有被其他線程持有,當前線程可以獲取該監視器,并將進入計數器加 1。如果監視器已經被其他線程持有,當前線程會被阻塞,直到該監視器被釋放。
(2)monitorexit 指令
當線程執行到 monitorexit 指令時,它會將監視器的進入計數器減 1。當進入計數器為 0 時,表示當前線程已經釋放了該監視器,其他線程可以嘗試獲取該監視器。
當使用 synchronized 修飾方法時,編譯后的字節碼會在方法的訪問標志中設置 ACC_SYNCHRONIZED 標志。當線程調用該方法時,會自動檢查該標志,如果設置了該標志,線程會首先嘗試獲取該方法所屬對象的監視器,然后再執行方法體。
【2】鎖升級原理
在 Java 6 之前,synchronized 是一個重量級鎖,性能較低。為了提高性能,Java 6 引入了鎖升級機制,使得 synchronized 的性能有了顯著提升。鎖升級的過程是一個逐步升級的過程,從無鎖狀態開始,經過偏向鎖、輕量級鎖,最終升級為重量級鎖。
(1)無鎖狀態
對象剛被創建時,處于無鎖狀態,Mark Word 中存儲的是對象的哈希碼、分代年齡等信息。
(2)偏向鎖
(1)原理:偏向鎖是為了在沒有競爭的情況下減少鎖的獲取和釋放帶來的性能開銷。當一個線程第一次訪問被 synchronized 修飾的代碼塊或方法時,會在對象頭的 Mark Word 中記錄該線程的 ID,這個過程稱為偏向鎖的獲取。以后該線程再次進入該同步代碼塊時,無需進行任何同步操作,直接進入代碼塊執行,這樣可以避免頻繁的鎖獲取和釋放操作,提高性能。
(2)升級條件:當有其他線程嘗試競爭該偏向鎖時,偏向鎖會升級為輕量級鎖。
(3)輕量級鎖
(1)原理:當線程獲取輕量級鎖時,會在當前線程的棧幀中創建一個鎖記錄(Lock Record),并將對象頭的 Mark Word 復制到鎖記錄中。然后,線程嘗試使用 CAS(Compare-And-Swap)操作將對象頭的 Mark Word 替換為指向鎖記錄的指針。如果替換成功,說明該線程成功獲取了輕量級鎖;如果替換失敗,說明有其他線程正在競爭該鎖,當前線程會嘗試自旋等待鎖的釋放。
(2)升級條件:如果自旋次數達到一定閾值(通常由 JVM 動態調整),或者有多個線程同時競爭該鎖,輕量級鎖會升級為重量級鎖。
(4)重量級鎖
原理:重量級鎖依賴于操作系統的互斥量(Mutex)來實現,當線程獲取重量級鎖失敗時,會被阻塞,進入等待隊列,直到鎖被釋放。重量級鎖的性能開銷較大,因為線程的阻塞和喚醒需要操作系統進行上下文切換,這會消耗較多的 CPU 時間。
【3】總結
synchronized 鎖的實現原理基于對象頭和監視器,通過 monitorenter 和 monitorexit 指令或 ACC_SYNCHRONIZED 標志來實現線程同步。鎖升級原理是為了在不同的競爭情況下選擇合適的鎖狀態,以提高性能。在無競爭的情況下,使用偏向鎖;在有少量競爭的情況下,使用輕量級鎖;在競爭激烈的情況下,使用重量級鎖。