1. 程式人生 > >《java並發編程實戰》讀書筆記6--取消與關閉

《java並發編程實戰》讀書筆記6--取消與關閉

特殊 指令 1.5 搶占 用法 tor wid cto hook

這章的主要內容是關於如何使任務和線程安全,快速,可靠的停止下來。

7.1 任務取消

在Java中沒有一種安全的搶占方式來停止線程,但是可以使用一些協作機制,比如:

技術分享

讓素數生成器運行1秒後取消(並不會剛好在運行1秒後停止,因為在請求取消的時刻和run方法中循環執行下一次檢查之間可能存在延遲):

技術分享

-7.1.1 中斷

上面的取消方法有個重要的問題是:如果任務中調用了一個阻塞方法,例如BlockingQueue.put,那麽任務可能永遠不會檢查取消標誌,因此永遠不會結束。比如:

技術分享

技術分享

前面第五章曾提到,一些特殊的阻塞庫的方法支持中斷。線程中斷是一種協作機制,線程可以通過這種機制來通知另一個線程,告訴它在合適的或者可能的情況下停止當前工作,並轉而執行其他的工作。

技術分享

阻塞方法庫,如Thread.sleep和Object.wait等,都會檢查線程何時中斷,並且在發現中斷時提前返回。

技術分享

技術分享

下面來解決之前BrokenPrimeProducer中永遠檢查不到標誌位的問題:使用中斷而不是boolean標誌來請求取消

技術分享

技術分享

-7.1.2 中斷策略

*對於非線程所有者的代碼來說(例如,對於線程池而言,任何在線程池實現以外的代碼),應該小心地保存中斷狀態,這樣擁有線程的代碼才能對中斷作出響應。

*大多數可阻塞的庫函數知識拋出interruptedException作為中斷響應,盡快退出流程,並把中斷信息傳遞給調用者,從而使調用棧中的上層代碼可以采取進一步的操作。

*當檢查到中斷請求時,任務並不需要放棄所有的操作——它可以推遲處理中斷請求,並直到某個更合適的時刻。因此需要記住中斷請求,並在完成當前任務後拋出InterruptedException或者表示已收到中斷請求。

技術分享

-7.1.3 響應中斷

技術分享

對於一些不支持取消但仍可以調用中斷的阻塞方法的操作,它們必須在循環中調用這些方法(interrput方法), 並在發現中斷後重新嘗試。在這種情況下應將中斷狀態保存在本地,並在返回前恢復狀態而不是在捕獲InterruptedException時恢復狀態:

技術分享

這部分有點看不明白。。。 先pass,以後再來鉆研鉆研

-7.1.4 示例:計時運行

給出了在指定時間內運行一個任意的Runnable的示例。在調用線程中運行任任務,並安排了一個取消任務,在運行指定的時間間隔後中斷它。這解決了從任務中拋出為檢查異常的問題,因為該異常會被timedRun的調用者捕獲。下面的程序用到了ScheduledExecutorService。ScheduledExecutorService定時周期執行指定的任務

技術分享

這是一種簡單的方法,但卻破壞了一下規則:在中斷線程之前,應該了解它的中斷策略。由於timedRun可以從任意一個線程調用,因此無法知道這個調用線程的中斷策略(說到這裏我好像把中斷策略所針對的對象搞混淆了)。

技術分享

技術分享

技術分享

在join方法返回後,它將檢查任務中是否有異常拋出( task.rethrow() ) 如果有的話則會在timeRunde的線程中再次拋出異常。執行任務的線程擁有自己的執行策略,即使任務不響應中斷,限時運行的方法(join)仍能返回到它的調用者。

join的不足:無法知道執行控制是因為線程正常退出而返回還是因為join超時而返回。

PS:這一小節看得有點雲裏霧裏的,漢化質量實在是太垃圾了,哪天翻翻英文原版書,看看這塊的解釋。

-7.1.5 通過Future來實現取消

技術分享

最後那句話是神馬意思? 醉了,語文沒學好的表示很蛋疼。

技術分享

-7.1.6 處理不可中斷的阻塞

在java的庫中,許多可阻塞的方法都是通過提前返回或者拋出InterruptedException來響應中斷請求的,從而使開發人員更容易構建出能響應取消請求的任務。然而,並非所有的可阻塞的方法或者阻塞機制都能響應中斷,中斷請求只能設置線程的中斷狀態,除此之外沒有任何其他作用。對於那些由於執行不可中斷操作而被阻塞的線程,可以使用類似於中斷的手段來停止這些線程,但這要求我們必須知道線程阻塞的原因。

技術分享

技術分享

技術分享

技術分享

-7.1.7 采用newTaskFor來封裝非標準的取消

newTaskFor是一個工廠方法,它將創建Future來代表任務,還能返回一個RunnableFuture接口,該接口擴展了Future和Runnable(並有FutureTask實現)。通過定制表示任務的Future可以改變Future.cancel的行為。那個程序來演示:

技術分享

技術分享

技術分享

這部分看得我有點雲裏霧裏的, 以後刷二周目的時候再來仔細分析分析。

7.2 停止基於線程的服務

*應用程序通常會創建擁有多個線程的服務

*應用程序可以擁有服務,服務可以擁有工作線程,但應用程序並不能擁有工作線程,因此應用程序不能直接停止工作線程。

-7.2.1 示例:日誌服務

技術分享

現在還需要實現一種終止日誌線程的方法,從而避免使JVM無法正常關閉。take方法能響應中斷,如果將日誌線程修改為當捕獲到InterruptedException時退出,那麽只需中斷日誌線程就能停止服務。然而,如果只是使日誌線程退出,那麽還不是一種完備的關閉機制。這種直接關閉的做法會丟失哪些正在等待被寫入到日誌的信息,而且其他線程在調用log時將被阻塞。另一種關閉LogWriter的方法是:設置某個“已請求關閉”標誌,以避免進一步提交日誌信息:

技術分享

在收到關閉請求後,消費者會把隊列中的所有消息寫入日誌,並解除所有在調用log時阻塞的生產者(我怎麽感覺這裏書這裏和代碼對不上號。。。)。然而這個方法中存在著競態條件問題,使得該方法並不可靠。向LogWriter添加可靠的取消操作:

技術分享

技術分享

-7.2.2 關閉ExecutorService

在復雜的程序中,通常會將ExecutorService封裝在某個更高級別的服務中,並且該服務能提供其自己的生命周期方法。如:

技術分享

技術分享

-7.2.3 “毒丸”對象

另一種關閉生產者-消費者服務的方式是使用“毒丸”對象,當得到這個對象時立即停止。“毒丸”對象確保消費者在關閉之前首先完成了隊列中的所有工作。來個例子:

技術分享

技術分享

技術分享

技術分享

只有在生產者和消費者的數量都已知的條件下,才可以使用“毒丸”對象。上述的解決方案可以擴展到多個生產者:只需每個生產者都向隊列中放入一個毒丸對象,並且消費者僅當在接收到Nproducers個毒丸對象時才停止。

-7.2.4 示例:只執行一次分服務

下面程序的checkMail方法能在多臺主機上並行地檢查新郵件。它創建一個私有的Executor,並向每臺主機提交一個任務。然後,當所有郵件檢查任務都執行完成後,關閉Executor並等待結束。

技術分享

ExecuteService提供的shutdown方法:平滑的關閉ExecutorService,當此方法被調用時,ExecutorService停止接收新的任務並且等待已經提交的任務(包含提交正在執行和提交未執行)執行完成。當所有提交任務執行完畢,線程池即被關閉。

awaitTermination方法:接收timeout和TimeUnit兩個參數,用於設定超時時間及單位。當等待超過設定時間時,會監測ExecutorService是否已經關閉,若關閉則返回true,否則返回false。一般情況下會和shutdown方法組合使用。

-7.2.5 shutdownNow的局限性

當通過shutdownNow來強行關閉ExecuteService時,它會嘗試取消正在執行的任務,並返回所有已提交但尚未開始的任務,從而將這些任務寫入日誌或者保存起來以便之後進行處理。但是,我們無法通過常規方法來找出哪些任務已經開始但尚未結束。意思就是雖然shutdownNow會返回所有已提交但尚未開始的任務(都是Runnable), 但是卻返回不了已經在執行但還未結束的任務,只能將這些任務取消掉。有的時候我們需要知道並保存這個狀態,所以不僅要知道哪些任務還沒有開始,而且還要知道哪些正在執行的任務還沒有完成。

下面的程序給出了如何在關閉過程中判斷正在執行的任務。

技術分享

技術分享

在程序清單7-22的WebCrawler中給出了TrackingExecutor的用法。網頁爬蟲程序的工作通常是無窮無盡的,因此當爬蟲程序關閉時,我們通常希望保存它的狀態(這裏的意思就是要保存程序正在處理的網頁),以便稍後重新啟功時繼續處理這些網頁。

技術分享

技術分享

在TrackingExecutor中存在一個不可避免的競態條件,從而產生誤報問題:一些認為已取消的任務實際上已經執行完成。原因是在任務執行最後一條指令以及線程池將任務記錄為“結束”的兩個時刻之間,線程池可能被關閉。如果任務是冪等的(即將任務執行兩次的結果與執行一次會得到相同的結果),那麽這不會存在問題,否則需考慮這種風險。

7.3 處理非正常的線程中止

首先應該明確的是導致線程提前死亡的最主要原因是RuntimeException,如何處理這種非正常的線程中止來確保多線程情況下的線程安全性呢?簡而言之就是利用捕獲異常處理器和故障通知機制。下面的程序給出了如何在線程池內部構建一個工作者線程:

技術分享

上面的是一種主動的方法來解決為檢查異常。在Thread API中提供了UncaughtExceptionHandler,它能檢測出由於未捕獲的異常而終結的情況。當一個線程由於未捕獲異常而退出時,JVM會把這個事件報告給UncaughtExceptionHandler異常處理器。

技術分享

最常見的響應方式是將一個錯誤信息以及相應的棧追蹤信息寫入應用程序中:

技術分享

要為線程池中的所有線程設置一個UncaughtExceptionHandler,需要為ThreadPoolExecutor的構造函數提供一個ThreadFactory。

7.4 JVM關閉

-7.4.1 關閉鉤子

正常的JVM關閉中,JVM首先調用所有已註冊的關閉鉤子。這個關閉鉤子指的是通過Runtime.addShutdownHook註冊但尚未開始的線程。後面看不明白了,pass....

-7.4.2 守護線程

線程可以分為兩種:普通線程和守護線程。在JVM啟動時創建的所有線程中,除了主線程以外,其他的線程都是守護線程(如垃圾回收器以及其他執行輔助工作的線程)。當創建一個新線程時,新線程將繼承創建它的線程的守護狀態,因此在默認情況下,主線程創建的所有線程都是普通線程。當一個線程退出時,JVM會檢查其他正在運行的線程,如果這些線程都是守護線程,那麽JVM會正常退出。當JVM停止時,所有仍然存在的守護線程都將被拋棄,既不會執行finally代碼塊,也不會執行回卷棧。

應該盡可能少使用守護線程——很少有操作能夠在不進行清理的情況下被安全地拋棄。特別是,如果在守護線程中執行可能包含I/O操作的任務將會是一種危險的行為。

-7.4.3 終結器

有些資源如文件句柄或套接字,當不需要時必須現實地交還給操作系統,而不是通過垃圾回收器回收。為了實現這個功能,垃圾回收器對哪些定義了finalize方法的對象會經行特殊處理:在回收器釋放它們後。調用它們的finalize方法,從而保證一些持久化的資源被釋放。大多數情況下,通過使用finally代碼塊和顯示的close方法,能夠比終結器更好的管理資源,所以要避免使用終結器。


技術分享

《java並發編程實戰》讀書筆記6--取消與關閉