小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Java 理論與實踐: 并發(fā)在一定程度上使一切變得簡單

 raydian 2007-01-10
對于每個項目,象許多其它應用程序基礎結構服務一樣,通常無需從頭重新編寫并發(fā)實用程序類(如工作隊列和線程池)。這個月,Brian Goetz 將介紹 Doug Lea 的 util.concurrent 包,這是一個高質量的、廣泛使用的、并發(fā)實用程序的開放源碼包??梢酝ㄟ^本文的 論壇提出您對本文的想法,以饗筆者和其他讀者。(您也可以單擊本文頂部或底部的“討論”參加論壇。)

當項目中需要 XML 解析器、文本索引程序和搜索引擎、正則表達式編譯器、XSL 處理器或 PDF 生成器時,我們中大多數(shù)人從不會考慮自己去編寫這些實用程序。每當需要這些設施時,我們會使用商業(yè)實現(xiàn)或開放源碼實現(xiàn)來執(zhí)行這些任務原因很簡單 ― 現(xiàn)有實現(xiàn)工作得很好,而且易于使用,自己編寫這些實用程序會事倍功半,或者甚至得不到結果。作為軟件工程師,我們更愿意遵循艾薩克?牛頓的信念 ― 站在巨人的肩膀之上,有時這是可取的,但并不總是這樣。(在 Richard Hamming 的 Turing Award 講座中,他認為計算機科學家的“自立”要更可取。)

探究重復發(fā)明“車輪”之原因

對于一些幾乎每個服務器應用程序都需要的低級應用程序框架服務(如日志記錄、數(shù)據(jù)庫連接合用、高速緩存和任務調度等),我們看到這些基本的基礎結構服務被一遍又一遍地重寫。為什么會發(fā)生這種情況?因為現(xiàn)有的選擇不夠充分,或者因為定制版本要更好些或更適合手邊的應用程序,但我認為這是不必要的。事實上,專為某個應用程序開發(fā)的定制版本常常并不比廣泛可用的、通用的實現(xiàn)更適合于該應用程序,也許會更差。例如,盡管您不喜歡 log4j,但它可以完成任務。盡管自己開發(fā)的日志記錄系統(tǒng)也許有一些 log4j 所缺乏的特定特性,但對于大多數(shù)應用程序,您很難證明,一個完善的定制日志記錄包值得付出從頭編寫的代價,而不使用現(xiàn)有的、通用的實現(xiàn)??墒?,許多項目團隊最終還是自己一遍又一遍地編寫日志記錄、連接合用或線程調度包。

表面上看起來簡單

我們不考慮自己去編寫 XSL 處理器的原因之一是,這將花費大量的工作。但這些低級的框架服務表面上看起來簡單,所以自己編寫它們似乎并不困難。然而,它們很難正常工作,并不象開始看起來那樣。這些特殊的“輪子”一直處在重復發(fā)明之中的主要原因是,在給定的應用程序中,往往一開始對這些工具的需求非常小,但當您遇到了無數(shù)其它項目中也存在的同樣問題時,這種需求會逐漸變大。理由通常象這樣:“我們不需要完善的日志記錄/調度/高速緩存包,只需要一些簡單的包,所以只編寫一些能達到我們目的的包,我們將針對自己特定的需求來調整它”。但情況往往是,您很快擴展了所編寫的這個簡單工具,并試圖添加再添加更多的特性,直到編寫出一個完善的基礎結構服務。至此,您通常會執(zhí)著于自己所編寫的程序,無論它是好是壞。您已經為構建自己的程序付出了全部的代價,所以除了轉至通用的實現(xiàn)所實際投入的遷移成本之外,還必須克服這種“已支付成本”的障礙。





回頁首


并發(fā)構件的價值所在

編寫調度和并發(fā)基礎結構類的確要比看上去難。Java 語言提供了一組有用的低級同步原語: wait() 、 notify()synchronized ,但具體使用這些原語需要一些技巧,需要考慮性能、死鎖、公平性、資源管理以及如何避免線程安全性方面帶來的危害等諸多因素。并發(fā)代碼難以編寫,更難以測試 ― 即使專家有時在第一次時也會出現(xiàn)錯誤。 Concurrent Programming in Java(請參閱 參考資料)的作者 Doug Lea 編寫了一個極其優(yōu)秀的、免費的并發(fā)實用程序包,它包括并發(fā)應用程序的鎖、互斥、隊列、線程池、輕量級任務、有效的并發(fā)集合、原子的算術操作和其它基本構件。人們一般稱這個包為 util.concurrent (因為它實際的包名很長),該包將形成 Java Community Process JSR 166 正在標準化的 JDK 1.5 中 java.util.concurrent 包的基礎。同時, util.concurrent 經過了良好的測試,許多服務器應用程序(包括 JBoss J2EE 應用程序服務器)都使用這個包。

填補空白

核心 Java 類庫中略去了一組有用的高級同步工具(譬如互斥、信號和阻塞、線程安全集合類)。Java 語言的并發(fā)原語 ― synchronization 、 wait()notify() ― 對于大多數(shù)服務器應用程序的需求而言過于低級。如果要試圖獲取鎖,但如果在給定的時間段內超時了還沒有獲得它,會發(fā)生什么情況?如果線程中斷了,則放棄獲取鎖的嘗試?創(chuàng)建一個至多可有 N 個線程持有的鎖?支持多種方式的鎖定(譬如帶互斥寫的并發(fā)讀)?或者以一種方式來獲取鎖,但以另一種方式釋放它?內置的鎖定機制不直接支持上述這些情形,但可以在 Java 語言所提供的基本并發(fā)原語上構建它們。但是這樣做需要一些技巧,而且容易出錯。

服務器應用程序開發(fā)人員需要簡單的設施來執(zhí)行互斥、同步事件響應、跨活動的數(shù)據(jù)通信以及異步地調度任務。對于這些任務,Java 語言所提供的低級原語很難用,而且容易出錯。 util.concurrent 包的目的在于通過提供一組用于鎖定、阻塞隊列和任務調度的類來填補這項空白,從而能夠處理一些常見的錯誤情況或者限制任務隊列和運行中的任務所消耗的資源。





回頁首


調度異步任務

util.concurrent 中使用最廣泛的類是那些處理異步事件調度的類。在本專欄七月份的文章中,我們研究了 thread pools and work queues,以及許多 Java 應用程序是如何使用“ Runnable 隊列”模式調度小工作單元。

可以通過簡單地為某個任務創(chuàng)建一個新線程來派生執(zhí)行該任務的后端線程,這種做法很吸引人:

new Thread(new Runnable() { ... } ).start();
            

雖然這種做法很好,而且很簡潔,但有兩個重大缺陷。首先,創(chuàng)建新的線程需要耗費一定資源,因此產生出許許多多線程,每個將執(zhí)行一個簡短的任務,然后退出,這意味著 JVM 也許要做更多的工作,創(chuàng)建和銷毀線程而消耗的資源比實際做有用工作所消耗的資源要多。即使創(chuàng)建和銷毀線程的開銷為零,這種執(zhí)行模式仍然有第二個更難以解決的缺陷 ― 在執(zhí)行某類任務時,如何限制所使用的資源?如果突然到來大量的請求,如何防止同時會產生大量的線程?現(xiàn)實世界中的服務器應用程序需要比這更小心地管理資源。您需要限制同時執(zhí)行異步任務的數(shù)目。

線程池解決了以上兩個問題 — 線程池具有可以同時提高調度效率和限制資源使用的好處。雖然人們可以方便地編寫工作隊列和用池線程執(zhí)行 Runnable 的線程池(七月份那篇專欄文章中的示例代碼正是用于此目的),但編寫有效的任務調度程序需要做比簡單地同步對共享隊列的訪問更多的工作?,F(xiàn)實世界中的任務調度程序應該可以處理死線程,殺死超量的池線程,使它們不消耗不必要的資源,根據(jù)負載動態(tài)地管理池的大小,以及限制排隊任務的數(shù)目。為了防止服務器應用程序在過載時由于內存不足錯誤而造成崩潰,最后一項(即限制排隊的任務數(shù)目)是很重要的。

限制任務隊列需要做決策 ― 如果工作隊列溢出,則如何處理這種溢出?拋棄最新的任務?拋棄最老的任務?阻塞正在提交的線程直到隊列有可用的空間?在正在提交的線程內執(zhí)行新的任務?存在著各種切實可行的溢出管理策略,每種策略都會在某些情形下適合,而在另一些情形下不適合。





回頁首


Executor

Util.concurrent 定義一個 Executor 接口,以異步地執(zhí)行 Runnable ,另外還定義了 Executor 的幾個實現(xiàn),它們具有不同的調度特征。將一個任務排入 executor 的隊列非常簡單:

Executor executor = new QueuedExecutor();
            ...
            Runnable runnable = ... ;
            executor.execute(runnable);
            

最簡單的實現(xiàn) ThreadedExecutor 為每個 Runnable 創(chuàng)建了一個新線程,這里沒有提供資源管理 ― 很象 new Thread(new Runnable() {}).start() 這個常用的方法。但 ThreadedExecutor 有一個重要的好處:通過只改變 executor 結構,就可以轉移到其它執(zhí)行模型,而不必緩慢地在整個應用程序源碼內查找所有創(chuàng)建新線程的地方。 QueuedExecutor 使用一個后端線程來處理所有任務,這非常類似于 AWT 和 Swing 中的事件線程。 QueuedExecutor 具有一個很好的特性:任務按照排隊的順序來執(zhí)行,因為是在一個線程內來執(zhí)行所有的任務,任務無需同步對共享數(shù)據(jù)的所有訪問。

PooledExecutor 是一個復雜的線程池實現(xiàn),它不但提供工作線程(worker thread)池中任務的調度,而且還可靈活地調整池的大小,同時還提供了線程生命周期管理,這個實現(xiàn)可以限制工作隊列中任務的數(shù)目,以防止隊列中的任務耗盡所有可用內存,另外還提供了多種可用的關閉和飽和度策略(阻塞、廢棄、拋出、廢棄最老的、在調用者中運行等)。所有的 Executor 實現(xiàn)為您管理線程的創(chuàng)建和銷毀,包括當關閉 executor 時,關閉所有線程,另外還為線程創(chuàng)建過程提供了 hook,以便應用程序可以管理它希望管理的線程實例化。例如,這使您可以將所有工作線程放在特定的 ThreadGroup 中,或者賦予它們描述性名稱。





回頁首


FutureResult

有時您希望異步地啟動一個進程,同時希望在以后需要這個進程時,可以使用該進程的結果。 FutureResult 實用程序類使這變得很容易。 FutureResult 表示可能要花一段時間執(zhí)行的任務,并且可以在另一個線程中執(zhí)行此任務, FutureResult 對象可用作執(zhí)行進程的句柄。通過它,您可以查明該任務是否已經完成,可以等待任務完成,并檢索其結果??梢詫?FutureResultExecutor 組合起來;可以創(chuàng)建一個 FutureResult 并將其排入 executor 的隊列,同時保留對 FutureResult 的引用。清單 1 顯示了一個一同使用 FutureResultExecutor 的簡單示例,它異步地啟動圖像著色,并繼續(xù)進行其它處理:


清單 1. 運作中的 FutureResult 和 Executor
  Executor executor = ...
            ImageRenderer renderer = ...
            FutureResult futureImage = new FutureResult();
            Runnable command = futureImage.setter(new Callable() {
            public Object call() { return renderer.render(rawImage); }
            });
            // start the rendering process
            executor.execute(command);
            // do other things while executing
            drawBorders();
            drawCaption();
            // retrieve the future result, blocking if necessary
            drawImage((Image)(futureImage.get())); // use future
            

FutureResult 和高速緩存

還可以使用 FutureResult 來提高按需裝入高速緩存的并發(fā)性。通過將 FutureResult 放置在高速緩存內,而不是放置計算本身的結果,可以減少持有高速緩存上寫鎖的時間。雖然這種做法不能加快第一個線程把某一項放入高速緩存,但它 減少第一個線程阻塞其它線程訪問高速緩存的時間。它還使其它線程更早地使用結果,因為它們可以從高速緩存中檢索 FutureTask 。清單 2 顯示了使用用于高速緩存的 FutureResult 示例:


清單 2. 使用 FutureResult 來改善高速緩存
public class FileCache {
            private Map cache = new HashMap();
            private Executor executor = new PooledExecutor();
            public void get(final String name) {
            FutureResult result;
            synchronized(cache) {
            result = cache.get(name);
            if (result == null) {
            result = new FutureResult();
            executor.execute(result.setter(new Callable() {
            public Object call() { return loadFile(name); }
            }));
            cache.put(result);
            }
            }
            return result.get();
            }
            }
            

這種方法使第一個線程快速地進入和退出同步塊,使其它線程與第一個線程一樣快地得到第一個線程計算的結果,不可能出現(xiàn)兩個線程都試圖計算同一個對象。





回頁首


結束語

util.concurrent 包包含許多有用的類,您可能認為其中一些類與您已編寫的類一樣好,也許甚至比從前還要好。它們是許多多線程應用程序的基本構件的高性能實現(xiàn),并經歷了大量測試。 util.concurrent 是 JSR 166 的切入點,它將帶來一組并發(fā)性的實用程序,這些實用程序將成為 JDK 1.5 中的 java.util.concurrent 包,但您不必等到那時侯才能使用它。在以后的文章中,我將討論 util.concurrent 中一些定制的同步類,并研究 util.concurrentjava.util.concurrent API 中的不同之處。


    本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多