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

來源:互聯網
上載者:User

標籤:actor   函數   追蹤   素數   raw   判斷   標誌位   常見   sub   

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

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是一個Factory 方法,它將建立Future來代表任務,還能返回一個RunnableFuture介面,該介面擴充了Future和Runnable(並有FutureTask實現)。通過定製表示任務的Future可以改變Future.cancel的行為。那個程式來示範:

                                                     

                                                    

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

 

7.2 停止基於線程的服務

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

*應用程式可以擁有服務,服務可以擁有背景工作執行緒,但應用程式並不能擁有背景工作執行緒,因此應用程式不能直接停止背景工作執行緒。

-7.2.1 樣本:Log Service

現在還需要實現一種終止日誌線程的方法,從而避免使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--取消與關閉

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.