併發07--執行緒池及Executor框架

mm發表於2020-06-28

一、JAVA中的執行緒池

執行緒池的實現原理及流程如下圖所示:

 

 

   如上圖所示,當一個執行緒提交到執行緒池時(execute()或submit()),先判斷核心執行緒數(corePoolSize)是否已滿,如果未滿,則直接建立執行緒執行任務;如果已滿,則判斷佇列(BlockingQueue)是否已滿,如果未滿,則將執行緒新增到佇列中;如果已滿,則判斷執行緒池(maximumPoolSize)是否已滿,如果未滿,則建立執行緒池執行任務;如果執行緒池已滿,則交給飽和策略(RejectedExecutionHandler.rejectExcution())來處理。

  可以看下執行緒池ThreadPoolExecutor的全參建構函式原始碼:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

對其入參釋義如下:

引數 描述 作用
coolPoolSize 執行緒核心執行緒數 當一個任務提交到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他的核心執行緒足夠執行新任務,也會建立執行緒,直到需要執行的任務數大於核心執行緒數後才不再建立;如果執行緒池先呼叫了preStartAllCoreThread()方法,則會先啟動所有核心執行緒。
maximumPoolSize 執行緒池最大執行緒數 如果佇列滿了,並且已建立的執行緒數小於該值,則會建立新的執行緒執行任務。這裡需要說明一點,如果使用的佇列時無界佇列,那麼該值無用。
keepAliveTime 存活時間 當執行緒池中執行緒超過超時時間沒有新的任務進入,則停止該執行緒;只會停止多於核心執行緒數的那幾個執行緒。
unit 執行緒存活的時間單位 可以有天、小時、分鐘、秒、毫秒、微妙、納秒
workQueue 任務佇列

用於儲存等待執行任務的阻塞佇列。可以選擇如下幾個佇列:陣列結構的有界佇列ArrayBlockingQueue、連結串列結果的有界佇列LinkedBlockingQueue、不儲存元素的阻塞佇列SynchronousQueue、一個具有優先順序的無界阻塞佇列PriortyBlockingQueue

threadFactory 建立執行緒的工廠

可以通過工廠給每個執行緒建立更有意義的名字。使用Guava提供的ThreadFactoryBuilder可以快速的給執行緒池裡的執行緒建立有意義的名字,程式碼如下

new ThreadFactoryBuilder().setNameFormat("aaaaaaaa").build();

handler 包和策略

當佇列和執行緒都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略來處理新提交的任務。

AbortPolicy(預設),表示無法處理新任務時丟擲異常。

CallerRunsPolicy:只有呼叫者所線上程來執行

DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務

DiscardPolicy:不處理,直接丟棄

  上面說到,向執行緒池提交任務有兩種方法,分別是execute()和submit(),兩者的區別主要是execute()提交的是不需要有返回值的任務,而submit提交的是需要有返回值的任務,並且submit()會返回一個Furure物件,並且可以使用future.get()方法獲取返回值,並且get方法會阻塞,直到有返回值。

  執行緒池的關閉有shutdown()和shutdownNow兩個方法,他們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫interrupt方法來中斷執行緒,所以無法中斷的執行緒可能永遠無法終止;但是二者也有區別,shutdownNow是將執行緒池的狀態設定為STOP,然後嘗試停止所有正在執行或者暫停的執行緒,並返回等待執行任務列表;而shutdown只是將執行緒池的狀態設定成SHUTDOWN,然後中斷所有沒有正在執行的任務。當呼叫這兩個方法中的任何一個後,isShutdown方法就會返回true,當所有任務都已經關閉後,呼叫isTerminaed方法會返回true。

  使用執行緒池時,需要從任務的性質(IO密集型還是CPU密集型或是混合型)、任務的優先順序、任務的執行時常、任務的依賴性(是否依賴其他系統資源,如資料庫連線等)來綜合判斷,比如說,CPU密集型,就可以就可以配置N+1個執行緒個數,其中N為CPU核數,如果是IO密集型,則可以配置2*N個執行緒數;如果是混合型的任務,可以將其拆分成IO密集型和CPU密集型,但是如果兩個任務的執行時間相差較大,則沒有必要進行拆分;優先順序不同的任務可以使用優先順序佇列PriortyBlockingQueue來處理;依賴資料出等其它資源的執行緒池,比如說依賴資料庫,那麼就可以加大執行緒數量,因為在等待sql執行的時候,執行緒是處於空閒狀態;另外,最好使用有界佇列,因為無界佇列,因為有界佇列可以增加系統的穩定性和預警能力。

  對於執行緒的監控,還有以下幾個方法可以使用:

方法 描述
taskCount() 執行緒池需要執行的任務數量
completedTskCount 執行緒池執行過程中已經執行完畢的任務數量
IarestPoolSize 執行緒池中曾經建立過的最大執行緒數
getPoolSize 執行緒池的執行緒數量
getActiveCount 獲取活動的執行緒數

二、Exector框架

   在java中,是用執行緒來非同步執行任務,java執行緒的建立與銷燬需要一定的開銷。如果我們為每一個任務建立一個執行緒的話,這些執行緒就會消耗大量的計算資源,會使處於高負荷的應用崩潰。

  在HotSpot虛擬機器中,JAVA執行緒被一對一的對映為本地作業系統執行緒。JAVA執行緒啟動時會建立一個本地作業系統執行緒,當該JAVA執行緒終止時,這個作業系統執行緒也會被收回,作業系統會呼叫多有執行緒並將他們分配給可用的CPU。

 

   Executor框架的兩級排程模型如上圖所示,應用程式通過Executor控制上層的排程,而下層的呼叫由作業系統核心控制,將執行緒對映到硬體處理器上,下層的呼叫不受應用程式的控制。

   關於Executor的組成部分如下所示:

元素 描述
任務 包括被執行任務需要實現的介面Runnable和Callable介面
任務的執行 包括任務執行機制的核心介面Executor,以及繼承自Executor的ExecutorService介面。Executor介面有兩個關鍵的實現類實現了ExecutorService介面:ThreadPoolExecutor和ScheduledThreadPoolExecutor
非同步計算的結果 包括介面Future和實現Future介面的FurureTask類

  Executor框架使用示意圖如下:

 

 

   如上圖所示,主執行緒首先建立實現Runnable或Callable介面的任務物件,然後把任務物件提交給ExecutorService執行,如果使用的是submit提交,執行完畢後將返回一個實現Future介面的物件,最後,主執行緒可以執行FutureTask.get()方法來獲取返回值;主執行緒也可以呼叫FutureTask.cancel()方法來取消此任務的執行。

  Executor框架的成員如下:

成員 描述 子類 描述
ThreadPoolExecutor

通常使用工廠類Executors來建立,Executors可以建立三種型別的ThreadPoolExecutor

固定執行緒數的FixedThreadPool

適用於為了滿足資源管理的需求,而需要限制當前執行緒數量的應用場景,它適用於負載比較重的應用。
單一執行緒的SingleThreadPool 適用於需要保證順序的執行各個任務,並且在任意時間點都不會有多個執行緒活動的場景。
根據需要建立執行緒的CacheThreadPool 這是一個無界的執行緒池,適用於執行很多短期非同步任務的小程式,或者是負載比較輕的伺服器。
ScheduledThreadPoolExecutor 通常使用工廠類Executors建立,Executors可以建立兩種型別的ScheduledThreadPoolExecutor 包含若干執行緒的ScheduledThreadPoolExecutor 適用於需要多個後臺執行緒執行週期任務,同時為了滿足資源管理的需求而需要限制後臺執行緒數量的應用場景。
只包含一個執行緒的SingleThreadScheduledExecutor                         適用於需要單個後臺執行緒執行週期任務,同時需要保證順序的執行各個任務的場景。                                               
ForkJoinsPool

 newWorkStealingPool適合使用在很耗時的操作,但是newWorkStealingPool不是ThreadPoolExecutor的擴充套件,它是新的執行緒池類ForkJoinPool的擴充套件,但是都是在統一的一個Executors類中實現,由於能夠合理的使用CPU進行對任務操作(並行操作),所以適合使用在很耗時的任務中 

   
Future Future介面和實現了該介面的FutureTask類來表示非同步計算的結果    
Runnable和Callable介面

Runnable和Callable介面的實現類,都可以被ThreadPoolExecutor、ScheduledThreadPool、ForkJoinThred執行;除了可以自己實現Callable介面外,我們還可以使用工廠類Executors來把一個Runnable包裝成一個Callable

   

 

ThreadPoolExecutor詳解

1、ThreadPoolExecutor

  (1)FixedThreadPool

  建構函式如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

  建構函式中,核心執行緒數和最大執行緒數一致,keepAliveTime為0,佇列使用的是無界阻塞佇列LinkedBlockingQueue(最大值是Integer.MAX_VALUE);

  核心執行緒數和最大執行緒數保持一致,表明:如果佇列滿了之後,不會再建立新的執行緒;

  keepAliveTime為0,表明:如果執行執行緒數大於核心執行緒數時,如果執行緒執行完畢,空閒執行緒立刻被終止;

  使用無界阻塞佇列,表明:當執行執行緒到達核心執行緒數時,不會再建立執行緒,只會將任務加入阻塞佇列;因此最大執行緒數引數無效;因此keepAliveTime引數無效;且不會拒絕任務(既不會執行包和策略)

  (2)SingleThreadExecutor

  建構函式如下:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

  建構函式中,核心執行緒數和最大執行緒數均為1,keepAliveTime為0,佇列使用的是無界阻塞佇列LinkedBlockingQueue(最大值是Integer.MAX_VALUE)

  除了固定了核心執行緒數和最大執行緒數為1外,其餘的引數均與FixedThreadPool一致,那麼就是隻有一個執行緒會反覆迴圈從阻塞佇列中獲取任務執行

  (3)CacheThreadPool

  建構函式如下:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

  建構函式中,核心執行緒數為0,最大執行緒數為Integer.MAX_VALUE,意味著無界,keepAliveTime為60秒,阻塞佇列使用沒有儲存空間的SynchronousQueue

  核心執行緒數為0,最大執行緒數為無界,表明:只要佇列滿了,就會建立新的執行緒放入執行緒池

  使用沒有儲存空間的SynchronousQueue表明:執行緒提交的速度高於執行緒被消費的速度,那麼執行緒會被不斷的建立,最終會因為執行緒建立過多而耗盡CPU和記憶體資源

2、ScheduledThreadPoolExecutor

  ScheduledThreadPoolExecutor的執行機制如下:

   (1)當呼叫ScheduledTreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法時會向ScheduledThreadPoolExecutor的DelayQueue新增一個實現了RunnableScheduledFuture介面的ScheduledFutureTask

  (2)執行緒池中的執行緒從DelayQueue中獲取ScheduledFutureTask,然後執行任務。

  ScheduledFutureTask主要包含以下三個成員變數

成員變數 描述
long time 表示這個任務要被執行的時間
long sequenceNumber 表示該任務被新增到ScheduledThreadPoolExecutor中的序號
long period 表示任務執行的間隔週期

  DelayQueue封裝了一個PriorityQueue,當新增任務時,這個PriorityQueue會對佇列中的ScheduledFutureTask進行排序,time最小的在最前面(最先被執行),如果time一致,就比較sequenceNumber,sequenceNumber小的排在前面。

  當執行緒執行任務時,先從DelayQueue佇列中獲取已經到期的任務(time大於當前時間),然後執行該任務,執行完畢後,根據任務的執行週期,修改任務下次的執行時間time,並重新將任務新增到DelayQueue

 

 

  FutureTask詳解

  Future介面和實現該介面的FutureTask類,代表非同步計算的結果。

  FutureTask的使用方法是將其交給Executor執行,也可以通過ExecutorService.submit()方法返回一個FutureTask,然後執行FutureTask.get()方法或FutureTask.cancel()方法,除此之外,還可以但是使用FutureTask。

  FutureTask有三種狀態:未啟動(FutureTask.run()沒有被執行之前的狀態)、已啟動(FutureTask.run()方法執行過程中)、已完成(FutureTask.run()方法執行完成或被取消),這三種狀態的流轉如下圖所示:

 

  FutureTask的實現是基於AQS(AbstractQueuedSynchrouizer)來實現的,之前已經說過,每一個基於AQS實現的同步器都會至少包含一個acquire操作和至少一個release操作。AQS被作為模板方法模式的基礎類提供給FutureTask的內部子類Sync實現了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通過這兩個方法來檢查和更新同步狀態。

  FutureTask涉及示意圖如下圖所示:

 

  如上圖所示,FutureTask.get()方法會呼叫AQS的acquireSharedInterruptibly(int)方法,該方法首先會回撥在子類Sync中的tryAcquireShared()方法來判斷acquire操作是否成功(state狀態狀態是否為執行完成RAN或取消狀態CANCELED&runner不為null),如果成功則get()方法立刻返回,如果失敗則到執行緒等待佇列中去等待其他執行緒執行release操作;當其他執行緒執行release操作(比如FutureTask.run()或FutureTask.cancel())喚醒當前執行緒後,當前執行緒再次執行tryAcquireShared()將返回正值1,當前執行緒將離開執行緒等待佇列並喚醒它的後繼執行緒。

  Run方法執行過程如下:

  執行在建構函式中指定的任務(Callable.call()),然後以原子方式來更新狀態(呼叫AQS.compareAndSetState(int expect, int update),設定state的狀態為RAN),如果這個原子操作成功,就設定代表計算結果的變數result的值為Callable.call()的返回值,然後呼叫AQS.release(int)。

  AQS.rease首先會呼叫子類Sync中實現的tryReleaseShared方法來執行release操作(設定執行任務的執行緒為null,然後返回false),然後喚醒等待佇列中的第一個執行緒。

  最後呼叫Future.done()方法。

 

相關文章