問題:多執行緒未帶來預期的效能提升#

當系統的效能(延遲或吞吐量)無法隨可用資源(如 CPU 核心數)線性擴展時,首先應關注 contention(爭奪)的來源,包括:

  • 未被平行化的函式
  • 鎖定共享資源的競爭
  • 記憶體快取問題(見 Item 65: False Sharing)

實際範例:Lock Contention#

一個 Java 程式使用多執行緒產生公私鑰對,並存入 HashMap。所有操作都在 synchronized(map) 區塊內執行:

synchronized(map) {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA", "SUN");
    // ... 產生金鑰對並放入 map
}

結果:4 個執行緒的執行時間(11.1 秒)和 1 個執行緒(11.0 秒)幾乎相同——完全沒有加速。

使用 Profiling 工具定位 Contention#

Java Flight Recorder#

  1. 收集資料:在 profiler 下執行程式
    $ java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder \
      -XX:StartFlightRecording=name=test,dumponexit=true,\
      filename=perf.jfr LockContention 1000 4
  2. 分析資料:透過 Oracle Java Mission Control GUI 檢視

從 Profiler 中觀察到的跡象#

  • 執行緒時間分配不均:某些執行緒的工作時間遠少於其他執行緒
  • 大量被阻塞的執行緒:執行緒花費數秒被 block
  • Stack trace 揭示瓶頸:最常被阻塞的執行緒的 stack trace 指向 HashMap 物件的 lock contention
  • 延遲來源:確認 12.9 秒的阻塞時間來自 HashMap 的鎖

解決方案#

HashMap 替換為 ConcurrentHashMap,移除過度的 synchronized 區塊:

  • 4 個執行緒的執行時間從 11.1 秒降至 3.5 秒
  • 相比單執行緒達到了約 3.2 倍加速

其他可用的 profiling 工具包括 Intel VTune Amplifier,也能有效定位 contention 問題。

重點回顧#

  • 多執行緒程式的可擴展性問題通常源於 lock contention——執行緒在爭奪共享資源時互相阻塞
  • 使用 profiling 工具(如 Java Flight Recorder、VTune)收集執行緒的阻塞時間和鎖資訊
  • 從 profiler 的資料中找出時間分配不均高阻塞率的執行緒,定位 contention 的來源
  • 常見的解法是將粗粒度的鎖替換為更細粒度的並行資料結構(如 ConcurrentHashMap