1. 程式人生 > >Java併發程式設計之結構化併發應用程式

Java併發程式設計之結構化併發應用程式

任務執行

    有三種任務執行的方式

    1.序列地執行任務,在任務執行策略裡,最簡單的就是在單個執行緒裡面序列地執行各項任務,但是這種策略理論上是正確的,在實際的生產環境裡的執行性非常的糟糕,因為它每次只能執行一個請求。主執行緒在執行的不同操作之間不斷地交替執行,而當單執行緒在等待一些操作的時候,CPU是空閒的,資源利用率非常的低。

    2.顯示地為任務建立執行緒,即每個請求都建立一個新的執行緒來提供服務,從而實現更高的響應性,比如Java併發程式設計之多執行緒實現中的前三個方法。雖然這個方法提高了更快的響應行跟吞吐率,但是也由一些缺陷,尤其是需要建立大量的執行緒時:1.執行緒生命週期的開銷非常高,執行緒的建立與銷燬都需要付出代價,根據平臺的不同,實際開銷也不同。2.資源消耗,活躍的執行緒消耗系統資源,尤其是記憶體。如果可執行的執行緒數量多於可用處理器的數量,那麼有些執行緒將閒置,而閒置的執行緒會大量的佔用記憶體。3.在建立執行緒的數量上有一個限制,這個限制值不同的平臺值也不同,它受很多因素影響,比如JVM中的啟動引數、Thread建構函式中請求的棧大小,以及底層系統對執行緒的限制等。如果破壞了這些限制,會很有可能丟擲OutOfMemorError異常。

    3.利用執行緒池來簡化 執行緒的管理工作。在Java類庫中,任務執行主要抽象不是Thread,而是Executor,Executor提供了一種標準的方法將任務的提交過程與執行過程解耦開來,並用Runnable來表示任務。Executor基於生產者-消費者模式,提交任務的操作相當於生產者,執行任務的執行緒相當於消費者。

Executor的生命週期

為了解執行服務的生命週期問題,Executor拓展了ExecutorService介面,添加了一些生命週期管理的方法:

1.execute(Runnable):接受一個Runnable,非同步執行。

2.submit(Runnale):返回一個Future物件,可以檢查提交的任務是否完成。

3.submit(Callable):返回一個Future物件,Callable介面中的call()方法有一個返回值,可以返回執行的結果

4.invokeAny():接受一個Callable引數,在任何一個任務完成成功就返回執行結果。

5.invokeAll():接受一個Callable引數,在全部任務完成後返回一個List

6.shutdown():執行平緩的關閉過程,不再接受新的任務,同時等待已經提交的任務執行完成,包括未開始的任務。

7.shutdownNow():粗暴的關閉過程,嘗試取消所有在執行中的任務,並且不再啟動佇列中尚未開始執行的任務。

延遲任務與週期任務

Timer類負責管理延遲任務以及週期任務,但是Timer由一些缺陷,Timer在執行所有定時任務時只會建立一個執行緒。如果某個任務的執行時間過長,那麼將破壞其他TimerTask的定時準確性。並且Timer並不捕獲異常,因此當TimerTask丟擲為未檢查的異常時將終止定時執行緒。這種情況下,Timer也不會恢復執行緒的執行,而是會錯誤的認為整個Timer都被取消了。因此已經被排程但尚未執行的TimerTask將不會執行,新的任務也不會被排程。這個問題也稱為“執行緒洩漏”。所以需要用ScheduledThreadPoolExecutor來處理這些表現出錯誤行為的任務。

執行緒池

建立執行緒要花費昂貴的資源和時間,如果任務來了才建立執行緒那麼響應時間會很長,而且一個程序能建立的執行緒數是有限的,為了避免這些問題,在程式啟動的時候就建立若干個執行緒來響應處理,它們被稱為執行緒池,裡面的執行緒稱為“工作執行緒”。工作執行緒的任務很簡單,從工作佇列中獲取一個任務,執行任務,然後返回執行緒池並等待下一個任務。可以通過Executor中的靜態工廠方法之一來建立一個執行緒池:

1、newFixedThreadPool:將建立一個固定長度的執行緒池,每當提交一個任務時就建立一個執行緒,直到達到執行緒池的最大數量,這時候執行緒池的規模將不再變化。

2、newCachedThreadPool:將建立一個可快取的執行緒池,如果執行緒池的當前規模超過了處理需求時,那麼將回收空閒的執行緒,而且當需求增加時,則可以新增新的執行緒,執行緒池的規模不存在任何限制。

3、newSingleThreadExecutor:是一個單執行緒的Executor,它建立單個工作者執行緒來執行任務,如果這個執行緒異常結束,會建立另一個執行緒來替代,它能確保依照任務在佇列中的順序來序列執行(例如FIFO、LIFO、優先順序)

4、newScheduledThreadPool:建立了一個固定長度的執行緒池,而且以延遲或者定時的方式來執行任務,類似Timer。

Executor框架可以任務的提交與任務的執行策略解耦開來,但是並非所有的任務都適用於所有的執行策略。有些型別的任務需要明確指定執行策略:1.依賴性任務:如果任務依賴於其他的任務,那麼就是隱含的給執行策略帶來了約束。2.使用執行緒封閉機制的任務:這種任務要求其執行所在的Executor是單執行緒的。如果將Executor從單執行緒環境改為執行緒池環境,那麼將會失去執行緒安全性。3.對響應時間敏感的任務。4.使用ThreadLocal的任務。

執行緒飢餓死鎖:如果所有正在執行任務的執行緒都由於等待其他仍處於工作佇列中的任務而阻塞,這種現象叫做執行緒飢餓死鎖:一個任務把另一個任務提交給同一個Executor,並且等待這個被提交任務的結果,第二個任務在等待第一個任務的結果,而第一個任務又無法完成,因為在等待第二個任務的完成,就發生死鎖。

設定執行緒池的大小:執行緒池過大,大量的執行緒將在相對很少的CPU和記憶體資源上發生競爭,這不僅僅導致更高的記憶體使用量,而且還可能耗盡資源。如果執行緒池過小,那麼將導致許多控線的處理器無法執行工作,從而降低吞吐率。

ThreadPoolExecutor允許提供一個BlockQueue來儲存等待執行的任務,排隊方法有三種:無界佇列、有界佇列和同步移交。

newFixedThreadPool和newSingleThreadExecutor在預設的情況下將使用一個無界的LinkedBlockingQueue,newCachedThreadPool使用了SynchronousQueue,它不是一個真正的佇列,而是一種執行緒之間進行移交的機制,要將一個元素放入SynchronousQueue中,必須有一個執行緒在等待接受這個元素。

而一種穩妥的管理策略是使用有界佇列,比如ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。有界佇列有助於避免資源耗盡,當佇列滿了的時候就要執行飽和策略:1.中止策略。2.呼叫者執行策略:將某些任務回退到呼叫者,從而降低新任務的流量。

執行緒工廠:每當執行緒池要建立一個執行緒時,都要通過執行緒工廠方法來完成。

任務取消與關閉

Thread中的interrput可以中斷目標執行緒,呼叫了這個方法並不意味著立即停止目標執行緒正在進行的工作,而是傳遞了請求中斷的訊息,isInterrupted方法返回目標執行緒的中斷狀態。靜態的interrupted方法將清除當前執行緒的中斷狀態,並返回它之前的值。

當嘗試取消某個任務時,不宜直接終端執行緒池,因為你並不知道當中斷請求到達時正在執行的是什麼任務,只能通過任務的Future來實現取消。

“毒丸”物件:關閉生產者消費者的方式就是使用“毒丸”物件,這個物件是放在佇列中的物件,當得到這個物件時,立即停止。這個物件將確保消費者在關閉之前首先完成佇列的所有工作,在提交“毒丸”物件之前提交的所有工作都會被處理,而生產者在提交了“毒丸”物件後不會再提交任何工作。只有在生產者和消費者數量已知的情況下才可以使用“毒丸”物件。

JVM的關閉:在正常關閉中,JVM首先呼叫所有已註冊的關閉鉤子。關閉鉤子是指通過Runtime.addShutdownHook

註冊的但尚未開始的執行緒。,如果執行緒還在執行,則跟這些關閉鉤子一起執行,當所有關閉鉤子都執行結束,如果runFinalizersOnExit為true時JVM將執行終結器,然後再停止。

JVM啟動建立的執行緒中,除了主執行緒以外其他都是守護執行緒。普通執行緒與守護執行緒的差別就在於當一個執行緒退出時,JVM會檢查其他正在執行的執行緒,如果這些都是守護執行緒,那JVM會正常退出操作,當JVM停止時,所有仍存在的守護執行緒都將會被拋棄。