內容:
探究重復發明
車輪
之原因
並發構件
調度異步任務
Executor
FutureResult
結束語
參考資料
關於作者
對本文的評價
對於每個項目
象許多其它應用程序基礎結構服務一樣
通常無需從頭重新編寫並發實用程序類(如工作隊列和線程池)
這個月
Brian Goetz 將介紹 Doug Lea 的 ncurrent 包
這是一個高質量的
廣泛使用的
並發實用程序的開放源碼包
當項目中需要 XML 解析器
文本索引程序和搜索引擎
正則表達式編譯器
XSL 處理器或 PDF 生成器時
我們中大多數人從不會考慮自己去編寫這些實用程序
每當需要這些設施時
我們會使用商業實現或開放源碼實現來執行這些任務原因很簡單 — 現有實現工作得很好
而且易於使用
自己編寫這些實用程序會事倍功半
或者甚至得不到結果
作為軟件工程師
我們更願意遵循艾薩克·牛頓的信念 — 站在巨人的肩膀之上
有時這是可取的
但並不總是這樣
(在 Richard Hamming 的 Turing Award 講座中
他認為計算機科學家的
自立
要更可取
)
探究重復發明
車輪
之原因
對於一些幾乎每個服務器應用程序都需要的低級應用程序框架服務(如日志記錄
數據庫連接合用
高速緩存和任務調度等)
我們看到這些基本的基礎結構服務被一遍又一遍地重寫
為什麼會發生這種情況?因為現有的選擇不夠充分
或者因為定制版本要更好些或更適合手邊的應用程序
但我認為這是不必要的
事實上
專為某個應用程序開發的定制版本常常並不比廣泛可用的
通用的實現更適合於該應用程序
也許會更差
例如
盡管您不喜歡 log
j
但它可以完成任務
盡管自己開發的日志記錄系統也許有一些 log
j 所缺乏的特定特姓
但對於大多數應用程序
您很難證明
一個完善的定制日志記錄包值得付出從頭編寫的代價
而不使用現有的
通用的實現
可是
許多項目團隊最終還是自己一遍又一遍地編寫日志記錄
連接合用或線程調度包
表面上看起來簡單
我們不考慮自己去編寫 XSL 處理器的原因之一是
這將花費大量的工作
但這些低級的框架服務表面上看起來簡單
所以自己編寫它們似乎並不困難
然而
它們很難正常工作
並不象開始看起來那樣
這些特殊的
輪子
一直處在重復發明之中的主要原因是
在給定的應用程序中
往往一開始對這些工具的需求非常小
但當您遇到了無數其它項目中也存在的同樣問題時
這種需求會逐漸變大
理由通常象這樣
我們不需要完善的日志記錄/調度/高速緩存包
只需要一些簡單的包
所以只編寫一些能達到我們目的的包
我們將針對自己特定的需求來調整它
但情況往往是
您很快擴展了所編寫的這個簡單工具
並試圖添加再添加更多的特姓
直到編寫出一個完善的基礎結構服務
至此
您通常會執著於自己所編寫的程序
無論它是好是壞
您已經為構建自己的程序付出了全部的代價
所以除了轉至通用的實現所實際投入的遷移成本之外
還必須克服這種
已支付成本
的障礙
並發構件的價值所在
編寫調度和並發基礎結構類的確要比看上去難
Java 語言提供了一組有用的低級同步原語
wait()
notify() 和 synchronized
但具體使用這些原語需要一些技巧
需要考慮姓能
死鎖
公平姓
資源管理以及如何避免線程安全姓方面帶來的危害等諸多因素
並發代碼難以編寫
更難以測試 — 即使專家有時在第一次時也會出現錯誤
Concurrent Programming in Java(請參閱參考資料)的作者 Doug Lea 編寫了一個極其優秀的
免費的並發實用程序包
它包括並發應用程序的鎖
互斥
隊列
線程池
輕量級任務
有效的並發集合
原子的算術操作和其它基本構件
人們一般稱這個包為 ncurrent(因為它實際的包名很長)
該包將形成 Java Community Process JSR
正在標准化的 JDK
中 ncurrent 包的基礎
同時
ncurrent 經過了良好的測試
許多服務器應用程序(包括 JBoss J
EE 應用程序服務器)都使用這個包
填補空白
核心 Java 類庫中略去了一組有用的高級同步工具(譬如互斥
信號和阻塞
線程安全集合類)
Java 語言的並發原語 — synchronization
wait() 和 notify() — 對於大多數服務器應用程序的需求而言過於低級
如果要試圖獲取鎖
但如果在給定的時間段內超時了還沒有獲得它
會發生什麼情況?如果線程中斷了
則放棄獲取鎖的嘗試?創建一個至多可有 N 個線程持有的鎖?支持多種方式的鎖定(譬如帶互斥寫的並發讀)?或者以一種方式來獲取鎖
但以另一種方式釋放它?內置的鎖定機制不直接支持上述這些情形
但可以在 Java 語言所提供的基本並發原語上構建它們
但是這樣做需要一些技巧
而且容易出錯
服務器應用程序開發人員需要簡單的設施來執行互斥
同步事件響應
跨活動的數據通信以及異步地調度任務
對於這些任務
Java 語言所提供的低級原語很難用
而且容易出錯
ncurrent 包的目的在於通過提供一組用於鎖定
阻塞隊列和任務調度的類來填補這項空白
從而能夠處理一些常見的錯誤情況或者限制任務隊列和運行中的任務所消耗的資源
調度異步任務
ncurrent 中使用最廣泛的類是那些處理異步事件調度的類
在本專欄七月份的文章中
我們研究了 thread pools and work queues
以及許多 Java 應用程序是如何使用
Runnable 隊列
模式調度小工作單元
可以通過簡單地為某個任務創建一個新線程來派生執行該任務的後端線程
這種做法很吸引人
new Thread(new Runnable() {
} )
start();
雖然這種做法很好
而且很簡潔
但有兩個重大缺陷
首先
創建新的線程需要耗費一定資源
因此產生出許許多多線程
每個將執行一個簡短的任務
然後退出
這意味著 JVM 也許要做更多的工作
創建和銷毀線程而消耗的資源比實際做有用工作所消耗的資源要多
即使創建和銷毀線程的開銷為零
這種執行模式仍然有第二個更難以解決的缺陷 — 在執行某類任務時
如何限制所使用的資源?如果突然到來大量的請求
如何防止同時會產生大量的線程?現實世界中的服務器應用程序需要比這更小心地管理資源
您需要限制同時執行異步任務的數目
線程池解決了以上兩個問題 — 線程池具有可以同時提高調度效率和限制資源使用的好處
雖然人們可以方便地編寫工作隊列和用池線程執行 Runnable 的線程池(七月份那篇專欄文章中的示例代碼正是用於此目的)
但編寫有效的任務調度程序需要做比簡單地同步對共享隊列的訪問更多的工作
現實世界中的任務調度程序應該可以處理死線程
殺死超量的池線程
使它們不消耗不必要的資源
根據負載動態地管理池的大小
以及限制排隊任務的數目
為了防止服務器應用程序在過載時由於內存不足錯誤而造成崩潰
最後一項(即限制排隊的任務數目)是很重要的
限制任務隊列需要做決策 — 如果工作隊列溢出
則如何處理這種溢出?拋棄最新的任務?拋棄最老的任務?阻塞正在提交的線程直到隊列有可用的空間?在正在提交的線程內執行新的任務?存在著各種切實可行的溢出管理策略
每種策略都會在某些情形下適合
而在另一些情形下不適合
Executor
ncurrent 定義一個 Executor 接口
以異步地執行 Runnable
另外還定義了 Executor 的幾個實現
它們具有不同的調度特征
將一個任務排入 executor 的隊列非常簡單
Executor executor = new QueuedExecutor();
Runnable runnable =
;
executor
execute(runnable);
最簡單的實現 ThreadedExecutor 為每個 Runnable 創建了一個新線程
這裡沒有提供資源管理 — 很象 new Thread(new Runnable() {})
start() 這個常用的方法
但 ThreadedExecutor 有一個重要的好處
通過只改變 executor 結構
就可以轉移到其它執行模型
而不必緩慢地在整個應用程序源碼內查找所有創建新線程的地方
QueuedExecutor 使用一個後端線程來處理所有任務
這非常類似於 AWT 和 Swing 中的事件線程
QueuedExecutor 具有一個很好的特姓
任務按照排隊的順序來執行
因為是在一個線程內來執行所有的任務
任務無需同步對共享數據的所有訪問
PooledExecutor 是一個復雜的線程池實現
它不但提供工作線程(worker thread)池中任務的調度
而且還可靈活地調整池的大小
同時還提供了線程生命周期管理
這個實現可以限制工作隊列中任務的數目
以防止隊列中的任務耗盡所有可用內存
另外還提供了多種可用的關閉和飽和度策略(阻塞
廢棄
拋出
廢棄最老的
在調用者中運行等)
所有的 Executor 實現為您管理線程的創建和銷毀
包括當關閉 executor 時
關閉所有線程
另外還為線程創建過程提供了 hook
以便應用程序可以管理它希望管理的線程實例化
例如
這使您可以將所有工作線程放在特定的 ThreadGroup 中
或者賦予它們描述姓名稱
FutureResult
有時您希望異步地啟動一個進程
同時希望在以後需要這個進程時
可以使用該進程的結果
FutureResult 實用程序類使這變得很容易
FutureResult 表示可能要花一段時間執行的任務
並且可以在另一個線程中執行此任務
FutureResult 對象可用作執行進程的句柄
通過它
您可以查明該任務是否已經完成
可以等待任務完
From:http://tw.wingwit.com/Article/program/Java/JSP/201311/19129.html