核心觀點#
作者透過幾個具體實作場景,深入探討導入平行化(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) 的應用#
引入平行化後,原本單純的伺服器類別可能會變臃腫。它可能同時負責:
- Socket 連線管理
- 客戶端請求的處理邏輯
- 執行緒的調度策略
- 伺服器關閉策略
設計建議:
為降低日後管理與修改難度,應將「平行化相關的程式碼」
(如執行緒管理)從「業務邏輯」中剝離,拆分到獨立的少量類別中。
案例二:執行路徑的複雜度#
看似簡單的程式碼#
考慮以下簡單的 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. 了解你的工具#
不要盲目使用多執行緒,應了解現有框架提供的解案:
| 工具 | 說明 |
|---|---|
| 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 無痛使用。