承接上篇,本文將針對 Java 6 鎖優化 (偏向鎖、鎖粗化、逸出分析、自適應自旋) 進行嚴謹的基準測試驗證,揭示為何同樣優化在不同處理器上效能差異巨大,並提供可復現的測試方法論。
微基準測試極易受干擾:JIT 編譯、垃圾回收 (GC)、OS 排程、CPU 降頻等都會扭曲結果。作者採用統計學方法:在穩定負載下,操作耗時應集中於固定區間;異常值代表外部干擾,需排查而非解釋。關鍵驗證工具包括:
-verbose:gc 監控 GC 影響-XX:+PrintCompilation 追蹤 JIT 編譯時機-XX:+TraceBiasedLocking 觀察偏向鎖啟動延遲HotSpot 會在方法調用達閾值 (-XX:CompileThreshold) 時觸發 JIT,並透過 On-Stack Replacement (OSR) 熱換程式碼,導致執行暫停。對策:測量前大量迴圈執行目標方法,直到 PrintCompilation 不再輸出新編譯記錄。
測試中創建 4 千萬字串物件,GC 必然頻繁。解法:-XX:NewSize=1500m -XX:SurvivorRatio=200000 擴大新生代,搭配 System.gc(); Thread.sleep(1000); 在迭代間強制全量 GC,並等待完成。注意:sleep 可能觸發 CPU 降頻,需在 BIOS 關閉節能功能。
偏向鎖非即時生效,通常延遲 3-4 秒。解法:延長預熱時間,或用 -XX:+TraceBiasedLocking 確認啟動點。
HotSpot 可能將無競爭鎖完全消除,進而觸發方法內聯、循環不變式外提、死碼消除 (DCE)。若 DCE 移除測試核心邏輯,結果將失真。對策:實作介面迷惑 JIT (interface Concat { ... }) 或使用 -XX:-Inline 禁用內聯。作者驗證內聯對本測試無額外影響。
命令:java -server -XX:+DoEscapeAnalysis -XX:+PrintCompilation -XX:+EliminateLocks -XX:+UseBiasedLocking -XX:+TraceBiasedLocking LockTest
輸出關鍵解讀:
s 表同步方法,% 表 OSR 編譯在 Intel Core 2 Duo 關閉一核 (單核模式) 測試:StringBuffer (同步) 與 StringBuilder (非同步) 效能幾乎持平。開啟雙核後,StringBuffer 基準耗時從 3731ms 飆升至 6574ms (+176%)。
核心原因:現代 CPU 使用本地快取降低存取延遲。鎖操作插入 存儲屏障,強制所有核心協調快取一致性,導致執行緒暫停。核心數越多、一致性模型越保守,協調成本越高。這正是偏向鎖、鎖粗化等優化在多核環境價值所在。
理論上,逸出分析若判斷物件不逸出執行緒,可消除同步鎖。但實測中,DoEscapeAnalysis 對 StringBuffer 基準測試完全無效。作者發現關鍵在於 被鎖物件的資料大小:
int index 欄位 (物件較大) → 逸出分析失效,同步開銷 208%Windows/Linux 表現一致;Mac OS X 則無論物件大小皆維持 120% 開銷,推測其實現更保守。結論:逸出分析仍處早期階段,觸發條件嚴苛且不穩定,不可仰賴於生產環境關鍵路徑。
// LockTest.java - StringBuffer vs StringBuilder
public class LockTest {
private static final int MAX = 20_000_000;
public static void main(String[] args) throws InterruptedException {
// Warm-up
for (int i = 0; i < MAX; i++) {
concatBuffer("Josh", "James", "Duke");
concatBuilder("Josh", "James", "Duke");
}
System.gc(); Thread.sleep(1000);
// Test StringBuffer
long start = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) concatBuffer("Josh", "James", "Duke");
long bufferCost = System.currentTimeMillis() - start;
// Test StringBuilder
System.gc(); Thread.sleep(1000);
start = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) concatBuilder("Josh", "James", "Duke");
long builderCost = System.currentTimeMillis() - start;
// Report
System.out.printf("StringBuffer: %d ms%nStringBuilder: %d ms%nOverhead: %.1f%%%n",
bufferCost, builderCost, (bufferCost * 100.0 / builderCost) - 100);
}
public static String concatBuffer(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString();
}
public static String concatBuilder(String s1, String s2, String s3) {
StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString();
}
}
// EATest.java - 逸出分析驗證
final class ThreadUnsafeObject { /* ... 非同步版 ... */ }
final class ThreadSafeObject { /* ... 同步版,含/不含未使用欄位 ... */ }
作者 Jeroen Borgers 為 Xebia 資深顧問,專精 Java 效能調優。致謝 Cliff Click、Kirk Pepperdine、David Dagastine 及 Xebia 同事協助驗證。英文原文:Do Java 6 threading optimizations actually work? - Part II。