核心觀點#

作者透過幾個具體實作場景,深入探討導入平行化(Concurrency)設計時的考量點。
平行化雖強大,但若不理解底層運作原理與邊界條件,易引入難察覺的錯誤。

主要探討三個面向:效能瓶頸的判斷、執行路徑的複雜度,以及如何處理相依方法的鎖定策略。


案例一:客戶端/伺服器應用程式 (Client/Server)#

效能瓶頸的診斷#

假設有個伺服器監聽 Socket 請求。如果系統無法在規定時間內(例如 10 秒)處理完,
我們須先判斷瓶頸在哪,才能決定是否引入平行化。

瓶頸類型特徵平行化效果
I/O 密集型
(I/O Bound)
使用 Socket、連結資料庫、
等待虛擬記憶體交換
有用。當一個執行緒等待 I/O 時,
另個執行緒可用 CPU 處理工作
處理器密集型
(CPU Bound)
複雜數值計算、正規表示式處理、
頻繁的垃圾回收 (GC)
無法直接解決
(甚至可能因 Context Switch 而變慢),
除非配合演算法優化或硬體升級

Figure A.1: Single thread

Figure A.2: Three concurrent threads

2. 單一職責原則 (SRP) 的應用#

引入平行化後,原本單純的伺服器類別可能會變臃腫。它可能同時負責:

  1. Socket 連線管理
  2. 客戶端請求的處理邏輯
  3. 執行緒的調度策略
  4. 伺服器關閉策略

設計建議:
為降低日後管理與修改難度,應將「平行化相關的程式碼」
(如執行緒管理)從「業務邏輯」中剝離,拆分到獨立的少量類別中。


案例二:執行路徑的複雜度#

看似簡單的程式碼#

考慮以下簡單的 ID 產生器:

public class IdGenerator {
    int lastIdUsed;

    public int incrementValue() {
        return ++lastIdUsed;
    }
}

驚人的路徑組合#

++lastIdUsed 看似只有一個動作,但在 Bytecode 層級,它包含了讀取、修改、寫入等多個指令。

  • 單執行緒: 只有 1 種執行路徑,結果確定

  • 多執行緒 (T 個): 假設該行程式碼轉換為 8 個 Bytecode 指令。
    當有 T 個執行緒同時執行時,可能的執行順序高達 (8T)! / ( 8!)^T 種。這是個天文數字

  • 加上 Synchronized:

    public synchronized void incrementValue() {
        ++lastIdUsed;
    }

路徑數量大幅收斂為 T! 種,確保了原子性(Atomicity)。

單行程式碼編譯後可能變成多行指令。
沒有適當保護的情況下,多個執行緒會在這些指令「間隙」中穿插執行,導致資料不一致。

設計時必須考量:

  1. 哪裡有共用物件?
  2. 哪些操作會導致讀取/更新衝突?
  3. 如何防止?

案例三:相依方法的鎖定策略#

1. 了解你的工具#

不要盲目使用多執行緒,應了解現有框架提供的解案:

工具說明
Executor Framework用執行緒池(Thread Pool)管理執行緒生命週期,
避免頻繁建立/銷毀的開銷
Non-blocking IO (NIO)用原子操作(如 CAS)或偵測數值變化更新資料,
而非傳統的「上鎖(Locking)」,效率通常較高
Thread-Safe Classes盡量用 Java 內建的執行緒安全類別
(如 ConcurrentHashMap)

2. 處理方法相依性 (Method Dependencies)#

當類別中的方法間存在相依性(例如:先檢查 hasNext() 再呼叫 next()),平行化可能會破壞邏輯。
以下是三種應對策略:

策略評價說明
容忍錯誤僅適用於非關鍵任務系統即便發生錯誤也不會造成傷害
客戶端鎖定
(Client-Side Locking)
不推薦呼叫者可能忘記鎖定
,違反 DRY 原則
伺服器端鎖定
(Server-Side Locking)
推薦在類別內部封裝鎖定邏輯
,減少重複、降低出錯機率

Figure A.3: Deadlock

伺服器端鎖定範例

若原本的 Iterator 不是執行緒安全的,我們不該讓每個使用它的 Client 自己加鎖,
而應該包裝一個新的ServerLockedIterator,在內部處理好所有同步邏輯,讓 Client 無痛使用。