在高併發編程領域,鎖是雙刃劍——保證線程安全的同時,往往成為效能瓶頸。本文系統梳理鎖優化的五大核心思路、JVM 內建的三層鎖升級機制、一個經典的 Integer 鎖失效案例,以及 ThreadLocal 作為無鎖化方案的原理深度解析。
核心原則:只在真正需要線程安全的代碼區塊加鎖,非關鍵路徑代碼移出同步塊。
// 優化前:整個方法加鎖,持有時間過長
public synchronized void syncMethod() {
otherCode1(); // 非線程安全操作
mutexMethod(); // 核心線程安全操作
otherCode2(); // 非線程安全操作
}
// 優化後:僅核心區塊加鎖
public void syncMethod() {
otherCode1();
synchronized(this) {
mutexMethod();
}
otherCode2();
}
將「大對象鎖」拆解為多個「小對象鎖」,減少線程爭用概率。典型代表:ConcurrentHashMap 的 Segment 分段鎖設計(JDK 7)或 CAS + synchronized 節點鎖(JDK 8+)。
根據功能語義將鎖拆分為讀鎖/寫鎖,實現「讀讀共存、讀寫互斥、寫寫互斥」。
反直覺策略:若對同一把鎖頻繁「獲取-釋放」,反而增加系統開銷。將多個相鄰同步塊合併為一個大同步塊,前提是中間非同步代碼執行極快。
// 優化前:循環內反覆獲取釋放鎖
for (int i = 0; i < CIRCLE; i++) {
synchronized(lock) { /* 簡短操作 */ }
}
// 優化後:鎖外移至循環外層
synchronized(lock) {
for (int i = 0; i < CIRCLE; i++) { /* 簡短操作 */ }
}
JIT 編譯器優化:逃逸分析發現對象不會逃出線程作用域,自動消除同步鎖。典型場景:StringBuffer、Vector 等線程安全類的局部變量使用。
// 開啟參數:-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // 局部變量,不逃逸
sb.append(s1);
sb.append(s2);
return sb.toString(); // 僅返回 String,sb 不逃逸
}
實測:開啟鎖消除後效能提升 2 倍以上(302ms vs 660ms)。
JVM 中每個對象都有對象頭,32 位系統下 Mark Word 存儲:HashCode、鎖狀態標記、GC 分代年齡、偏向鎖線程 ID、指向 Monitor 的指針等。
-XX:+UseBiasedLocking(默認開啟),-XX:BiasedLockingStartupDelay=0 取消 4 秒延遲-XX:+UseSpinning → JDK 1.7+ 內建自適應自旋| 鎖類型 | 核心機制 | 適用場景 | 升級條件 |
|---|---|---|---|
| 偏向鎖 | Mark Word 記錄線程 ID,無 CAS | 單線程反覆進入同步塊 | 其他線程競爭 |
| 輕量級鎖 | CAS + 棧帧 Lock Record | 低競爭、短持有時間 | CAS 多次失敗 |
| 自旋鎖 | 用戶態循環嘗試 CAS | 極短持有時間、中等競爭 | 自旋次數耗盡 |
| 重量級鎖 | OS Mutex 互斥量 | 高競爭、長持有時間 | 最終兜底 |
本質上三者都是樂觀鎖思想:儘量避免昂貴的 OS 線程掛起/喚醒。
public class IntegerLock {
static Integer i = 0; // Integer 是不可變對象!
static class AddThread extends Thread {
public void run() {
for (int k = 0; k < 100000; k++) {
synchronized (i) { // 每次 i++ 都會產生新 Integer 對象!
i++;
}
}
}
}
// 兩個線程實際鎖住的是不同對象 → 線程不安全
}
根因: Integer 為 final 不可變類,i++ 實際是 i = Integer.valueOf(i.intValue() + 1),每次生成新對象,導致兩線程同步的是不同鎖對象。
修正: 改用 AtomicInteger 或鎖住 共享可變對象(如 Object lock = new Object())。
「以空間換時間」:為每個線程提供獨立的變量副本,徹底消除共享變量競爭,從而不需要鎖。
// 錯誤示範:共享同一個 SimpleDateFormat
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// ThreadLocal 正確用法
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();
public void parseDate(int i) {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 每線程 new 一個
}
Date t = tl.get().parse("2016-02-16 17:00:" + i % 60);
}
關鍵點: 必須為每個線程 new 獨立實例,不能共享同一個實例給所有 ThreadLocal。
Thread 內部持有 ThreadLocalMap threadLocalsThreadLocalMap 類似 HashMap,但 哈希衝突用線性探測(索引++)而非鏈表 解決set():當前線程 Map 存入 this(ThreadLocal) → valueget():從當前線程 Map 取 this 對應 valueHibernate 經典應用:ThreadLocal 綁定當前線程的 Session,實現「一請求一 Session」模式。
鎖優化是系統性工程:應用層講究粒度、分離、消除;JVM 層提供偏向→輕量級→自旋→重量級四階段自適應升級;設計層可用 ThreadLocal 徹底規避共享狀態。掌握這三個維度,才能在高併發場景下遊刃有餘。