在 Java 多執行緒開發中,synchronized 關鍵字是最基礎的同步手段。但許多開發者在選擇鎖物件時掉入陷阱,導致本應獨立的鎖變成全局共享鎖,嚴重影響併發效能甚至產生邏輯錯誤。本文通過一個反面教材,深度剖析鎖物件選擇的核心原則,並給出業界公認的最佳實踐方案。
public class SyncTest {
private String name = "name"; // 字面量進入常量池
public void method(String flag) {
synchronized (name) { // 危險!鎖定的是常量池中共享的 "name" 物件
System.out.println(flag + ", invoke method ....");
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
public static void main(String[] args) {
SyncTest test1 = new SyncTest();
SyncTest test2 = new SyncTest(); // 兩個不同實例
// 預期:test1 的兩線程同步、test2 的兩線程同步、test1/test2 互不干擾
// 實際:四線程全串行,因為鎖住的是同一個常量池 "name" 物件
}
}
"name" 放入字串常量池(String Constant Pool)。String.intern() 機制)。test1.name 與 test2.name 指向同一個 String 物件。synchronized(name) 實際鎖定的是這個共享物件,導致所有實例共用同一把鎖。後果:原本應該獨立的實例級同步變成了全局串行,併發效能暴跌,業務邏輯可能錯誤。
| 物件類型 | 風險原因 | 示例 |
|---|---|---|
String 字面量 / intern() | 常量池全局唯一 | synchronized("lock") |
包裝類型常量(Integer -128~127、Boolean、Byte、Short、Character 0~127) | JVM 快取池全局唯一 | synchronized(Integer.valueOf(100)) |
Class 物件 | 每個類僅一個 Class 實例 | synchronized(MyClass.class)(若非有意做類級鎖) |
| 不可變物件被多處引用 | 如共享的配置物件、單例物件 | synchronized(sharedConfig) |
public class SyncTest {
// 方案 A:私有 final Object(語義清晰,佔用 16 bytes 對象頭)
private final Object lock = new Object();
// 方案 B:零長度 byte 陣列(推薦,佔用 16 bytes 對象頭 + 0 bytes 數據,無數組複製開銷)
private final byte[] lock = new byte[0];
public void method() {
synchronized (lock) { // 每個實例擁有獨立鎖物件
// 臨界區代碼
}
}
}
為什麼推薦 new byte[0]?
new Object(),陣列物件在某些 JIT 編譯器中更易被標記為非逃逸,鎖消除優化機會更大。Arrays.copyOf 等操作修改結構,天然安全。static final Object / Class 物件(有意為之)public class Singleton {
private static final Object CLASS_LOCK = new Object(); // 或直接用 MyClass.class
public static void staticMethod() {
synchronized (CLASS_LOCK) { // 所有實例、所有線程共享此鎖
// 靜態資源保護
}
}
}
synchronized 修飾實例/靜態方法(簡單場景)// 實例方法鎖:鎖住 this
public synchronized void instanceMethod() { ... }
// 靜態方法鎖:鎖住 Class 物件
public static synchronized void staticMethod() { ... }
注意:方法級鎖粒度較粗,整個方法體為臨界區,高併發下建議縮小同步塊範圍。
// 差:整個方法同步,包含耗時 I/O、睡眠
public synchronized void badMethod() {
doQuickWork();
Thread.sleep(1000); // 鎖持有期間阻塞其他線程
doMoreWork();
}
// 好:僅保護共享狀態讀寫
public void goodMethod() {
doQuickWork();
synchronized (lock) {
updateSharedState(); // 僅共享狀態操作加鎖
}
Thread.sleep(1000); // 釋放鎖後睡眠
doMoreWork();
}
// 線程 1:synchronized(lockA) -> synchronized(lockB)
// 線程 2:synchronized(lockB) -> synchronized(lockA) // 死鎖風險
// 解決:全局統一獲取順序(如按 hashCode 或 id 排序)
public void transfer(Account from, Account to, int amount) {
Object firstLock = from.hashCode() < to.hashCode() ? from : to;
Object secondLock = firstLock == from ? to : from;
synchronized (firstLock) {
synchronized (secondLock) {
// 安全轉賬
}
}
}
ReentrantLock / StampedLock 替代ReentrantLock:支援公平鎖、可中斷、超時獲取、多條件變量(Condition)。StampedLock(Java 8+):樂觀讀鎖、讀寫分離,適合讀多寫少場景,效能優於 ReentrantReadWriteLock。String 字面量、無小整數包裝類、無共享不可變物件。ReentrantLock、StampedLock、LongAdder 等無鎖方案。實例級同步首選
private final byte[] lock = new byte[0];,類級同步用private static final Object lock = new Object();,拒絕 String 常量、包裝類快取、共享不可變物件作為鎖。