《Java併發程式設計實戰》學習筆記 任務執行和取消關閉

weixin_33766168發表於2016-05-05

第六章 任務執行

大多數併發應用程式是圍繞執行任務進行管理的。設計任務時,要為任務設計一個清晰的任務邊界,並配合一個明確的任務執行策略。任務最好是獨立的,因為這會提高併發度。大多數伺服器應用程式都選擇了下面這個自然的任務邊界:單個客戶請求。
任務時邏輯上的工作單元,執行緒是使任務非同步執行的機制。
應用程式內部的任務排程,存在多種可能的排程策略:
其中,最簡單的策略是在單一的執行緒中順序的執行任務。但它的吞吐量和響應性很差,一般只在特殊情況下使用:任務的數量很少但生命週期很長時,或者伺服器只服務於唯一的使用者時,伺服器在同一時間內只需同時處理一個請求。
每任務一個執行緒(thread-per-task)。在中等強度的負載下,“每任務一個執行緒”的方法是對順序化執行的良好改進。但它存在一些實際的缺陷,因為它會無限制的建立執行緒,建立/關閉執行緒是需要開銷的,同時執行緒還會消耗系統資源,而且會影響穩定性。所以應該限制可建立執行緒的數目。
使用執行緒池——Executor框架。如同有界佇列,Executor可以防止應用程式過載而耗盡資源,而且Executor是基於生產者-消費者模式的,可以分離任務提交和任務執行。如果要在你的程式中實現一個生產者-消費者的設計,使用Executor通常是最簡單的方式。
使用Executor的一個優點是:要改變程式的執行,只要改變Executor的實現就行,也就是任務的執行,不需要動任務的提交,而且Executor的實現是放在一個地方的,但任務的提交則是擴散到整個程式中。
執行策略是資源管理工具,最佳策略取決於可用的計算資源和你對服務質量的需求。一個執行策略指明瞭任務執行的“what,where,when,how”幾個因素,具體包括:
任務在什麼(what)執行緒中執行?
任務以什麼(what)順序執行(FIFO,LIFO,優先順序)?
可以有多少個(how many)任務併發執行?
可以有多少個(how many)任務進入等待執行佇列?
如果系統過載,需要放棄一個任務,應該挑選哪一個(which)任務?另外,如何(how)通知應用程式知道這一切呢?
在一個任務的執行前與結束後,應該做什麼(what)處理?
Executor的生命週期
Executor有三種狀態:執行、關閉、終止。建立後的初始狀態是執行狀態,shutdown()方法會啟動一個平緩的關閉過程,shutdownNow()方法會啟動一個強制的關閉過程。在關閉後提交到Executor中的任務,會被被拒執行處理器(RejectedExecutionHandler)處理(可能只是簡單的放棄)。一旦所有的任務全部完成後,Executor迴轉入終止狀態,可以呼叫awaitTermination等待,或者isTerminated判斷。
可以使用scheduledThreadPoolExecutor代替Timer使用,Timer存在一些缺陷:Timer只建立唯一的執行緒來執行所有timer任務;Timer丟擲的未檢查的異常會終止timer執行緒,而且Timer也不會再重新恢復執行緒的執行了。
Executor框架讓制定一個執行策略變得簡單,不過想要使用Executor,你還必須能夠將你的任務描述為Runnable。
Runnable、Callable、Future比較
Runnable是執行任務的抽象,但它的run方法不能返回一個值或者跑出受檢查的異常;Callable是更佳的抽象,它的run方法有返回值,並能丟擲異常;而Future提供了相關的方法來獲得任務的結果、取消任務以及檢驗任務是否已經完成還是被取消。
Executor的所有submit方法都返回一個Future,用它可以重新獲得任務執行的結果,或者取消任務。除此之外,在Java 6中,ExecutorService的所有實現都可以重寫newTaskFor方法,把Callable封裝成Future。
將程式的任務量分配到不同的任務中:當存在大量的相互獨立、同類的能夠併發處理的任務時,效能才能真正的提升;否則,效能提升的相當少,甚至降低效能。
當有一批任務需要Executor處理時,使用completionService更方便,而且還可以使用take方法,獲取完成的任務(可以沒完成一個取一個,提高併發)。如果不需要邊完成邊去結果的話,處理批任務還可以使用Executor.InvokeAll方法。
總結
圍繞任務的執行來構造應用程式,可以簡化開發,便於同步。Executor框架有助於分離任務的提交和任務的執行策略,同時還支援很多不同型別的執行策略。每當你要為執行任務而建立執行緒時,可以考慮使用Executor。為了最大化效益,在把應用程式分解為不同的任務時,你必須確定一個合乎情理的任務邊界。在一些應用程式中,存在明顯的工作良好的任務邊界,然而還有一些程式,你需要作進一步的分析,以揭示更多可行的併發。


第七章 取消和關閉

任務取消:當外部程式碼能夠在活動自然完成之前,把它更改為完成狀態,那麼這個活動被稱為可取消的。活動取消的原因:使用者請求取消、限時活動、應用程式事件、錯誤、關閉。
Java沒有提供任何機制,來安全的強迫執行緒停止手頭的工作。它提供了中斷——一個協作機制,是一個執行緒能夠要求另一個執行緒停止當前的工作。任務和服務可以這樣編碼:當要求它們停止時,它們首先清除當前程式中的工作,然後再終止。因而需要一個取消策略。
取消策略,取消的how、when、what:其他程式碼如何請求取消該任務,任務在什麼時候檢查取消的請求是否到達,響應取消請求的任務中應有的行為。
在API和語言規範中,並沒有把中斷與任何取消的語意繫結起來,但是,實際上,使用中斷來處理取消之外的任何事情都是不明智的,並且很難支撐起更大的應用。
呼叫interrupt並不意味著必然停止目標執行緒正在進行的工作;它僅僅傳遞了請求中斷的訊息。我們對中斷本身最好的理解應該是:它並不會真正中斷一個正在執行的執行緒;它僅僅發出中斷請求,執行緒自己會在下一個方便的時刻中斷(取消點)。
中斷通常是實現取消最明智的選擇。
因為每一個執行緒都有其自己的中斷策略,所以你不應該中斷執行緒,除非你知道中斷對這個執行緒意味著什麼。
呼叫可中斷的阻塞函式時,如Thread.sleep、BlockingQueue.put,有兩種處理InterruptedException的實用策略:
傳遞異常(很可能發生在清除特定任務後),使你的方法也成為可中斷的阻塞方法;
或者恢復中斷狀態,從而上層呼叫棧中的程式碼能夠對其進行處理。
只有實現了執行緒中斷策略的程式碼才可以接受中斷請求。一般性的任務和程式庫程式碼不應該接受中斷請求。
當Future.get丟擲InterruptedException或TimeoutException時,如果你知道不再需要結果時,就可以呼叫Future.cancel來取消任務了。
被不可中斷活動阻塞的執行緒,我們可以用類似於中斷的技術停止它們,但這更需要明確執行緒阻塞的原因。
對於擁有執行緒的服務,只要服務的生存時間大於建立執行緒的方法的生存時間,就需要提供生命週期方法。
生產者-消費者服務關閉的方法:
自己提供生命週期方法,生產者原子的新增工作,比較難。
使用ExecutorService類:封裝ExecutorService,在內部程式碼中呼叫ExecutorService的生命週期方法——shutdown、shutdownNow。在非生產者-消費者中也適用。
使用毒丸:一個可識別的物件,置於佇列中,意味著“當你得到它時,停止一切工作”。要停止服務時,中斷生產者——生產者的中斷處理中向每一個消費者新增一個毒丸,消費者碰到毒丸,停止工作。
如果一個方法需要處理一批任務,並在所有任務結束前不會返回,那麼他可以通過使用私有的Executor來簡化服務的生命週期管理,其中Executor的生命限定在該方法中。
shutdownNow的侷限性:它試圖取消正在進行的任務,並返回那些等待執行的任務的清單,但是我們沒法找出那些已經開始執行、卻沒有結束的任務,這需要自己處理。
在一個長時間執行的應用程式中,所有的程式都要給未捕獲異常設定一個處理器,這個處理器至少要將異常資訊記入日誌中。
執行緒分為兩種:普通執行緒和守護執行緒。兩者的區別是:當一個執行緒退出時,所有仍然存在的守護執行緒都會被拋棄——不會執行finally塊,也不會釋放棧——JVM直接退出。
管理資源避免使用finalizer。在大多數情況下,使用finally塊和顯式close方法結合來管理資源,會比使用finalizer起到更好的作用。
總結
任務、執行緒、服務以及應用程式在生命週期結束時的問題,可能會導致向它們引入複雜的設計和實現。Java沒有提供具有明顯優勢的機制來取消活動或者終結執行緒。它提供了協作的中斷機制,能夠用來幫助取消,但是這將取決你如何構建取消的協議,並是否能一致的使用該協議。使用FutureTask和Executor框架可以簡化構建可取消的任務和服務。


相關文章