當專案需要 XML 解析器、全文檢索引擎、正則表達式編譯器、XSLT 處理器或 PDF 生成器時,我們多數人從不考慮自行開發——成熟的商業或開源實現觸手可用,工作良好且易於使用。自行開發事倍功半,甚至無法產出可用結果。身為軟體工程師,我們樂於「站在巨人的肩膀上」(牛頓語),但這並非總是最佳選擇(Richard Hamming 曾在圖靈獎演講中主張計算機科學家應具備「自立」精神)。
幾乎每個伺服器應用都需要的低層框架服務——日誌記錄、資料庫連接池、快取、任務調度——卻在專案中被一遍遍重寫。原因在於現有選擇不夠完善,或認為定制版本更適合手邊應用。但我認為這多數是不必要的:專為某應用開發的定制版本,往往不比通用實現更適合,甚至更差。例如雖然你不喜歡 log4j,但它能完成任務;自製日誌系統或許有 log4j 缺乏的特定功能,但對大多數應用,難以證明從頭寫一套完善的定制日誌包值得付出的成本。然而,無數專案團隊仍在重複編寫日誌、連接池、執行緒調度套件。
我們不自寫 XSLT 處理器是因為工作量龐大。但這些低層框架服務「表面看起來簡單」,讓人以為自行開發不難。實則它們很難正確運作,遠非初看那樣。這類特殊「車輪」持續被重複發明的主因是:專案初期對這類工具需求極小,隨著遇到其他專案同樣面臨的問題,需求逐漸增長。典型心聲:「我們不需要完善的日誌/調度/快取套件,只需簡單能用的,針對特定需求調整即可。」但很快你就會擴展這個簡單工具,不斷加入新功能,直到寫出一套完善的基礎設施服務。此時你會執著於自製程式碼,無論好壞——你已付出全部建設成本,除遷移成本外,還須克服「已沉沒成本」心理障礙。
編寫調度與並發基礎設施類比想像中難得多。Java 提供了 wait()、notify() 和 synchronized 等低層同步原語,但具體使用需兼顧效能、死鎖、公平性、資源管理、執行緒安全等諸多因素。並發程式碼難寫、更難測試——專家初次嘗試也常出錯。《Concurrent Programming in Java》作者 Doug Lea 開發了優秀的免費並發工具包 util.concurrent,包含鎖、互斥、佇列、執行緒池、輕量級任務、高效並發集合、原子算術操作等基本組件。此包經充分測試,被 JBoss 等伺服器應用廣泛使用,並成為 JSR 166 標準化的 JDK 1.5 java.util.concurrent 套件基礎。
核心 Java 類庫缺乏高層同步工具(互斥、信號、阻塞佇列、執行緒安全集合)。Java 並發原語(synchronization、wait()、notify())對伺服器應用過於低層。例如:嘗試獲取鎖但超時放棄?執行緒中斷時放棄獲取鎖?創建最多 N 個執行緒可持有的鎖?支援多種鎖定模式(如讀寫鎖)?以一種方式獲取鎖、另一種方式釋放?內建鎖機制不直接支援這些場景,雖可在低層原語上構建,但需技巧且易出錯。
伺服器應用開發者需要簡單設施執行互斥、同步事件響應、跨活動傳遞資料、非同步調度任務。util.concurrent 旨在提供鎖定、阻塞佇列、任務調度類填補此空白,處理常見錯誤情況、限制任務佇列與執行中任務資源消耗。
util.concurrent 最廣泛使用的類別是處理非同步事件調度的類別。簡單為任務創建新執行緒(new Thread(new Runnable() {...}).start())雖簡潔但有兩大缺陷:
執行緒池解決這兩問題——提高調度效率並限制資源使用。雖可自行編寫簡單工作佇列與執行緒池,但現實任務調度器需處理死執行緒、回收閒置池執行緒、根據負載動態調整池大小、限制排隊任務數量(防止過載時 OutOfMemoryError 導致崩潰)。
限制任務佇列需決策溢出策略:丟棄最新任務?丟棄最舊任務?阻塞提交執行緒?在提交執行緒內執行新任務?各策略適用不同場景。
util.concurrent 定義 Executor 介面非同步執行 Runnable,並提供多種實現:
Runnable 創建新執行緒,無資源管理,類似 new Thread(...).start()。優點:僅改變 executor 結構即可切換執行模型,無需全域搜尋替換執行緒創建代碼。ThreadGroup、描述性名稱)。有時需非同步啟動流程,稍後取得結果。FutureResult 讓這變簡單:表示耗時任務,可在另一執行緒執行,物件可作執行句柄——查詢是否完成、等待完成、取得結果。可與 Executor 結合:創建 FutureResult 並排入佇列,保留引用稍後取得結果。範例:非同步啟動圖像渲染,同時繼續繪製邊框、標題,最後取得渲染結果繪製圖像。
Executor executor = ...;
ImageRenderer renderer = ...;
FutureResult futureImage = new FutureResult();
Runnable command = futureImage.setter(new Callable() {
public Object call() { return renderer.render(rawImage); }
});
executor.execute(command);
drawBorders();
drawCaption();
drawImage((Image)(futureImage.get())); // 取得結果,必要時阻塞
將 FutureResult 而非計算結果置入快取,可減少持有快取寫鎖時間。雖無法加速第一個執行緒放入快取,但減少第一個執行緒阻塞其他執行緒存取快取的時間,其他執行緒可更早從快取取得 FutureTask 使用結果。範例:檔案快取中使用 FutureResult,首個執行緒快速進出同步區塊,其他執行緒同樣快速取得結果,避免重複計算同一物件。
util.concurrent 包含許多有用類別,或許與你自寫的同樣好、甚至更好。它們是多執行緒應用基本組件的高效能實現,經大量測試驗證。此包是 JSR 166 切入點,將帶來 JDK 1.5 java.util.concurrent 套件,但你無需等待即可使用。後續文章將討論其中定制同步類別,並比較 util.concurrent 與 java.util.concurrent API 差異。