Sun、IBM、BEA 等廠商在 Java 6 中投入大量資源優化鎖管理與同步機制,推出偏向鎖、鎖粗化、逸出分析導致的鎖消除、自適應自旋鎖等特性,承諾透過更高效的線程間資料共享提升併發效能。但這些優化在實際場景中效果如何?本文透過單線程基準測試,逐一剖析核心優化機制並呈現初步測試結果。
Java 採用悲觀鎖:即便線程競爭極罕見,仍強制透過同步保護共享資料。研究顯示多數鎖無競爭,但請求鎖仍觸發昂貴的原子指令與系統開銷。經典案例:StringBuffer 為線程安全設計,但實際常用於單線程場景,JDK 1.5 引入非同步的 StringBuilder 即為回應此痛點。JVM 開發者嘗試在運行時透過 HotSpot/JIT 解決:「我們真的需要這把鎖嗎?」
逸出分析屬於 HotSpot 分析工作一環。若物件引用被限制在區域範圍內,不「逸出」至方法外部,則其他線程無法存取,保護該物件的鎖即可安全消除。
public String concatBuffer(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer(); // sb 僅在方法內部使用
sb.append(s1); sb.append(s2); sb.append(s3);
return sb.toString(); // sb 不會逸出
}
此例中 sb 為區域變數,所有引用不逸出,理論上同步鎖可被 JIT 忽略。
大多數鎖生命週期中僅被單一線程存取。偏向鎖將鎖「偏向」首次獲得的線程:該線程後續進入同步區塊無需執行昂貴的原子指令獲取契約,僅需簡單檢查。若其他線程嘗試競爭,偏向線程釋放契約即可。JDK 6 預設啟用 (-XX:+UseBiasedLocking)。
將相鄰的多個同步區塊合併為單一較大同步區塊,減少重複獲取/釋放鎖的開銷。例:StringBuffer.append 呼叫四次同步方法,內聯後可合併為單一鎖操作。風險:過度粗化延長臨界區,可能阻塞其他合法線程,因此循環內的鎖不會被粗化至整個循環。
傳統自旋鎖固定自旋 10 次再掛起線程。JDK 6 引入自適應機制:自旋時長動態調整,基於同一鎖上次自旋成功與否及持有者狀態。成功則延長 (如 100 圈),失敗則縮短甚至放棄,避免浪費 CPU 週期。
測試核心:大量字串拼接 (2000 萬次),緩衝區初始容量足夠,最小化臨界區工作,聚焦同步開銷測量。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);
System.out.println("Starting test");
// StringBuffer test
long start = System.currentTimeMillis();
for (int i = 0; i < MAX; i++) concatBuffer("Josh", "James", "Duke");
long bufferCost = System.currentTimeMillis() - start;
// StringBuilder test
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%nThread safety overhead: %.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();
}
}
-server),JRE 不含,需 JDK-XX:+DoEscapeAnalysis (預設關)、-XX:+UseBiasedLocking (預設開)、-XX:+EliminateLocks (預設開)
StringBuilder 效能穩定,不受鎖優化影響 (符合預期)。StringBuffer 約慢 3 倍 (同步開銷基準)。EliminateLocks 提供微幅提升。StringBuilder 基準線。優化確能移除大部分鎖開銷,但結果不穩定且受硬體影響大。下篇將深入探討:統計驗證方法、JIT/GC 干擾排除、單核與多核差異根因、逸出分析失效原因分析,並給出最終實務建議。
作者 Jeroen Borgers 為 Xebia 資深顧問,專精 Java 效能調優。致謝 Cliff Click、Kirk Pepperdine、David Dagastine 及 Xebia 同事。英文原文:Do Java 6 threading optimizations actually work? - Part I。