Executor框架(Java併發編程的藝術筆記)

Executor框架簡介

Executor框架的兩級調度模型

在HotSpot VM的線程模型中,Java線程被一對一映射爲本地操作系統線程,Java線程啓動時會創建一個本地OS線程;當該Java線程終止時,這個OS線程也會被回收。OS會調度所有線程並將它們分配給可用的CPU。

在上層,Java多線程程序通常把應用分解爲若干個任務,然後使用用戶級的調度器(Excutor框架)將這些任務映射爲固定數量的線程;在底層,OS內核(OSKernel)將這些線程映射到硬件處理器上。

由下圖可看出,應用程序通過Executor框架控制上層的調度;而下層的調度由操作系統 內核控制,下層的調度不受應用程序的控制。

 

Executor框架的結構與成員

Executor框架主要由3大部分組成:

  • 任務。包括被執行任務需要實現的接口:Runnable接口或Callable接口
  • 任務的執行。包括任務執行機制的核心接口Executor,以及繼承自Executor的 ExecutorService接口。Executor框架有兩個關鍵類實現了ExecutorService接口 (ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
  • 異步計算的結果。包括接口Future和實現Future接口的FutureTask類

Executor框架包含的主要的類與接口如下所示,

(1)Executor是一個接口,它是Executor框架的基礎,它將任務的提交與任務的執行分離。

(2)ThreadPoolExecutor是線程池的核心實現類,用來執行被提交的任務。

(3)ScheduledThreadPoolExecutor是一個實現類,可以在給定的延遲後運行命令,或者定期執行命令。ScheduledThreadPoolExecutor比Timer更靈活,功能更強大。

(4)Future接口和實現Future接口的FutureTask類,代表異步計算的結果。

(5)Runnable接口和Callable接口的實現類,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。

 

                                                                                  Executor框架的使用

(1)主線程首先要創建實現Runnable或者Callable接口的任務對象。工具類Executors可以把一 個Runnable對象封裝爲一個Callable對象(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。

(2)以把Runnable對象直接交給ExecutorService執行(ExecutorService.execute(Runnable command));或者也可以把Runnable對象或Callable對象提交給ExecutorService執行(ExecutorService.submit(Runnable task)或ExecutorService.submit(Callabletask))。

如果執行ExecutorService.submit(…),ExecutorService將返回一個實現Future接口的對象 。由於FutureTask實現了Runnable,程序員也可以創建FutureTask,然後直接交給ExecutorService執行。

(3)主線程可以執行FutureTask.get()方法來等待任務執行完成。主線程也可以執行 FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。

                                                                                  Executor框架的主要成員

(1)ThreadPoolExecutor

ThreadPoolExecutor通常使用工廠類Executors來創建。Executors可以創建3種類型的 ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。

1)FixedThreadPool

創建使用固定線程數的方法如下。它適用於爲滿足資源管理,而需要限制當前線程數量的應用場景。如負載較重的服務器。

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)

2)SingleThreadExecutor

創建使用單個線程的方法如下。它適用於需要保證順序地執行各個任務;並且在任意時間點,不會有多 個線程是活動的應用場景。

public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)

3)CachedThreadPool

創建一個會根據需要而創建新線程的方法如下。它是大小無界的線程池,適用於執行很多的短期異步任務的小程序,或者 是負載較輕的服務器。

public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

(2)ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor通常使用工廠類Executors來創建。Executors可以創建2種類型的ScheduledThreadPoolExecutor:

1)ScheduledThreadPoolExecutor

它適用於需要多個後臺線程執行週期任務,同時爲了滿足資源管理的需求而需要限制後臺線程數量的場景。它包含若干個線程

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize,ThreadFactory factory

2)SingleThreadScheduledExecutor

它適用於需要單個後臺線程執行週期任務,同時需要保證順序地執行各個任務的應用場景。

public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newSingleThreadScheduledExecutor
(ThreadFactory threadFactory)

(3)Future接口

Future接口和實現Future接口的FutureTask類用來表示異步計算的結果。當我們把Runnable 接口或Callable接口的實現類提交(submit)給ThreadPoolExecutor或 ScheduledThreadPoolExecutor時,ThreadPoolExecutor或ScheduledThreadPoolExecutor會向我們 返回一個FutureTask對象。

<T> Future<T> submit(Callable<T> task)
<T> Future<T> submit(Runnable task, T result)
Future<> submit(Runnable task)

注:截止至JDK8爲止,Java通過上述API返回的是一個 FutureTask對象。但Java僅僅保證返回的是一個實現了Future接口的對象。在將 來的JDK實現中,返回的可能不一定是FutureTask。

(4)Runnable接口和Callable接口

Runnable接口和Callable接口的實現類,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。它們之間的區別是Runnable不會返回結果,而Callable可以返回結果。

可以使用工廠類Executors來把一個 Runnable包裝成一個Callable。

public static Callable<Object> callable(Runnable task)

我們也可以把一個Runnable和一個待返回的結果包裝成一個Callable

public static <T> Callable<T> callable(Runnable task, T result)

 


ThreadPoolExecutor詳解

Executor框架最核心的類是ThreadPoolExecutor,它是線程池的實現類,主要的參數如下:

  • corePool:核心線程數量。 即使沒有任務執行,核心線程也會一直存活
  • maximumPool:最大線程池的大小。
  • keepAliveTime:如果當前線程池中線程數大於corePoolSize,則多餘的線程在等待keepAliveTime時間後如果還沒有新的線程任務指派給它、它就會被回收。
  • BlockingQueue:用來暫時保存任務的工作隊列。
  • RejectedExecutionHandler:當ThreadPoolExecutor已經關閉或ThreadPoolExecutor已經飽和 時(達到了最大線程池大小且工作隊列已滿),execute()方法將要調用的Handler。

ThreadPoolExecutor有3種類型·FixedThreadPool,SingleThreadExecutor,CachedThreadPool。下面分別介紹

 

FixedThreadPool詳解

它是可重用固定線程數的線程池,構造它的靜態方法如下

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

FixedThreadPool的corePoolSize和maximumPoolSize參數都被設置爲創建FixedThreadPool時指 定的參數nThreads。

當線程池中的線程數大於corePoolSize時,keepAliveTime爲多餘的空閒線程等待新任務的 最長時間,超過這個時間後多餘的線程將被終止。這裏把keepAliveTime設置爲0L,意味着多餘 的空閒線程會被立即終止。

FixedThreadPool的execute()方法說明

  1. 當前運行的線程少於corePoolSize,則創建新線程執行任務。
  2. 當前運行的線程數等於corePoolSize,將任務加入LinkedBlockingQueue。
  3. 線程執行第1步中的任務後,會在循環中反覆從LinkedBlockingQueue獲取任務來執行。

FixedThreadPool使用無界隊列LinkedBlockingQueue作爲線程池的工作隊列(隊列的容量爲 Integer.MAX_VALUE)。

  1. 當線程池中的線程數達到corePoolSize後,新任務將在無界隊列中等待。因此線程池中的線程數不會超過corePoolSize。
  2. 使用無界隊列時maximumPoolSize將是一個無效參數
  3. 使用無界隊列時keepAliveTime將是一個無效參數
  4. 由於使用無界隊列,運行中的FixedThreadPool不會拒絕任務(不會調用RejectedExecutionHandler.rejectedExecution方法)。

 

SingleThreadExecutor詳解

它是使用單個worker線程的Executor,靜態構造方法如下:

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

corePoolSize和maximumPoolSize被設置爲1。其他參數與 FixedThreadPool相同。

SingleThreadExecutor的execute()說明

  1. 如果當前運行的線程數少於corePoolSize(即線程池中無運行的線程),則創建一個新線程來執行任務。
  2. 當前線程池中有一個運行的線程,將任務加入入LinkedBlockingQueue。
  3. 線程執行完第一步的任務後,會在一個無限循環中反覆從LinkedBlockingQueue獲取任務來 執行。

 

CachedThreadPool詳解

它是一個會根據需要創建新線程的線程池,下面是創建的靜態方法。

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

corePoolSize被設置爲0,即corePool爲空;maximumPoolSize被設置爲 Integer.MAX_VALUE,即maximumPool是無界的;keepAliveTime設置爲60L,表示CachedThreadPool中的空閒線程 如果等待新任務超過60秒後將會被終止。

CachedThreadPool使用沒有容量的SynchronousQueue作爲線程池的工作隊列,但 CachedThreadPool的maximumPool是無界的。因此,如果主線程提交任務的速度高於 maximumPool中線程處理任務的速度時,CachedThreadPool會不斷創建新線程。極端情況下, CachedThreadPool會因爲創建過多線程而耗盡CPU和內存資源。

SynchronousQueue是一個沒有容量的阻塞隊列。每個插入操作必須等待另一個線程的對應移除操作

CachedThreadPool的execute()方法說明

  1. 執行SynchronousQueue.offer(Runnable task)。如果當前maximumPool中有空閒線程 正在執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那麼空閒線程成功從隊列取任務。execute()方法執行完成,否則執行下面步驟。
  2. 當初始maximumPool爲空,或者maximumPool中當前沒有空閒線程時,沒有線程執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。此時CachedThreadPool會創建一個新線程執行任務,execute()方法執行完成。
  3. 在上步中新創建的線程執行完任務後,會執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),該操作讓空閒線程最多再隊列中等待60s。

 


ScheduledThreadPoolExecutor詳解

該類繼承自ThreadPoolExecutor,主要用來在給定的延遲之後運行任務,或者定期執行任務。

ScheduledThreadPoolExecutor可以在構造函數中指定多個對應的後臺線程數,而Timer對應的是單個後臺線程。

ScheduledThreadPoolExecutor的運行機制

ScheduledThreadPoolExecutor使用的是支持延時獲取元素的無界阻塞隊列DelayQueue。

ScheduledThreadPoolExecutor執行主要分爲兩部分:

  1. 當調用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法時,會向ScheduledThreadPoolExecutor的DelayQueue添加一個實現了 RunnableScheduledFutur接口的ScheduledFutureTask。
  2. 線程池中的線程從DelayQueue中獲取ScheduledFutureTask,然後執行任務。

ScheduledThreadPoolExecutor的實現

ScheduledThreadPoolExecutor會把待調度的任務(ScheduledFutureTask) 放到一個DelayQueue中。

ScheduledFutureTask主要包含3個成員變量:

  • long型成員變量time,表示這個任務將要被執行的具體時間。
  • long型成員變量sequenceNumber,表示這個任務被添加到ScheduledThreadPoolExecutor中 的序號。
  • long型成員變量period,表示任務執行的間隔週期。

DelayQueue封裝了一個PriorityQueue(優先級隊列),這個PriorityQueue會對隊列中的ScheduledFutureTask進行排序。排序時,time小的排在前面(時間早的任務將被先執行)。如果兩個 ScheduledFutureTask的time相同,就比較sequenceNumber,sequenceNumber小的排在前面(如果兩個任務的執行時間相同,那麼先提交的任務將被先執行)。

ScheduledThreadPoolExecutor的任務執行步驟

  1. 線程1從從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大於等於當前時間,並執行任務。
  2. 線程1修改ScheduledFutureTask的time變量便爲下次將要被執行的時間,並放入DelayQueue中(DelayQueue.add())。

DelayQueue.take()方法

獲取任務步驟如下:

  1. 獲取鎖
  2. 獲取週期任務,若PriorityQueue爲空,當前線程到Condition中等待;否則執行下一步
  3. 若PriorityQueue頭元素的time參數比當前時間大,到Condition中等待直至time時間;否則執行下一步
  4. 獲取PriorityQueue的頭元素,若PriorityQueue不爲空,則喚醒在Condition中等待的所有線程。
  5. 釋放鎖
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); // 1
        try {
            for (;;) {
                E first = q.peek();
                if (first == null) {
                    available.await(); // 2
                } else {
                    long delay = first.getDelay(TimeUnit.NANOSECONDS);
                    if (delay > 0) {
                        long tl = available.awaitNanos(delay); // 3
                    } else {
                        E x = q.poll(); // 4
                        assert x != null;
                        if (q.size() != 0)
                            available.signalAll(); // 4
                        return x;
                    }
                }
            }
        } finally {
            lock.unlock(); // 6
        }
    }

ScheduledThreadPoolExecutor在一個循環中反覆執行步驟2,直到線程從隊列獲取到一個元素後,才退出循環。

DelayQueue.offer()方法

添加任務步驟如下:

  1. 獲取Lock
  2. 向PriorityQueue添加任務
  3. 若上一步添加的任務是隊列的頭元素,則喚醒在Condition中等待的所有線程
  4. 釋放鎖
    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock(); // 1
        try {
            E first = q.peek();
            q.offer(e); // 2
            if (first == null || e.compareTo(first) < 0)
                available.signalAll(); // 3
            return true;
        } finally {
            lock.unlock(); // 4
        }
    }

 


FutureTask詳解

FutureTask簡介

FutureTask除了實現Future接口外,還實現了Runnable接口。因此,FutureTask可以交給 Executor執行,也可以由調用線程直接執行(FutureTask.run())。當FutureTask.run()方法被執行時,FutureTask有下面3種狀態:

  • 未啓動。FutureTask.run()方法還沒有被執行之前,FutureTask處於未啓動狀態。當創建一 個FutureTask,且沒有執行FutureTask.run()方法之前,這個FutureTask處於未啓動狀態。
  • 已啓動。FutureTask.run()方法被執行的過程中,FutureTask處於已啓動狀態。
  • 已完成。FutureTask.run()方法執行完後正常結束,或被取消(FutureTask.cancel(…)),或 執行FutureTask.run()方法時拋出異常而異常結束,FutureTask處於已完成狀態。

get()方法和cancel()之間的影響

  • 當FutureTask處於未啓動或已啓動狀態時,執行FutureTask.get()方法將導致調用線程阻塞。
  • 當FutureTask處於已啓動狀態時,執行FutureTask.cancel(true)方法將以中斷執行此任務線程 的方式來試圖停止任務;
  • 當FutureTask處於已啓動狀態時,執行FutureTask.cancel(false)方法將 不會對正在執行此任務的線程產生影響(讓正在執行的任務運行完成);
  • 當FutureTask處於已完 成狀態時,執行FutureTask.cancel(…)方法將返回false。

 

FutureTask的使用

當一個線程需要等待另一個線程把某任務執行完後,他才能繼續執行,此時可以用FutureTask。

測試:假設有多個線程執行若干個任務,每個任務最多隻被執行一次,且只允許一個線程執行,其他線程需等待這個任務執行完後,才能繼續執行。這些若干任務存儲在currentHashMap

    final ConcurrentHashMap<Object, Future<String>> taskCache = new ConcurrentHashMap<>();

    String executionTask(final String taskName) throws Exception{
        while (true) {
            Future<String> future = taskCache.get(taskName);    // 1.1   2.1
            if(future == null) {
                Callable<String> task = new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        return taskName;
                    }
                };
                FutureTask<String> futureTask = new FutureTask<String>(task);  // 1.2
                future = taskCache.putIfAbsent(taskName, future);             // 1.3   
                if(future == null) {
                    futureTask.run();                                       //1.4
                }
            }
                return future.get();                // 1.5

        }
    }

代碼執行示意圖如下。當兩個線程試圖同時執行同一任務時,假設t1執行1.3後,t2執行2.1,則t2需在2.2中等待,直到t1執行完1.4後,t2才能從FutureTask.get()返回。

 

FutureTask的實現

FutureTask的實現基於AbstractQueuedSynchronizer(簡稱爲AQS)。java.util.concurrent中 的很多可阻塞類(比如ReentrantLock)都是基於AQS來實現的。基於於AQS實現的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、 CountDownLatch和FutureTask。

每一個基於AQS實現的同步器都包括兩種類型的操作:

  • 至少一個基於acquire操作這個操作阻塞調用線程,除非/直到AQS的狀態允許這個線程繼續 執行。FutureTask的acquire操作爲get()/get(long timeout,TimeUnit unit)方法調用。
  • ·至少一個release操作。這個操作改變AQS的狀態,改變後的狀態可允許一個或多個阻塞 線程被解除阻塞。FutureTask的release操作包括run()方法和cancel(…)方法。

FutureTask聲明瞭一個內部私有的繼承於AQS的子類 Sync,Sync實現了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通過這 兩個方法來檢查和更新同步狀態。

如上圖所示。FutureTask.get()方法會調用AQS.acquireSharedInterruptibly(int arg)方法,該方法執行過程如下:

  1. 調用AQS.acquireSharedInterruptibly(int arg)方法,這個方法首先會回調在子類Sync中實 現的tryAcquireShared()方法來判斷acquire操作是否可以成功。acquire操作可以成功的條件爲: state爲執行完成狀態RAN或已取消狀態CANCELLED,且runner不爲null。
  2. 如果成功則get()方法立即返回。如果失敗則到線程等待隊列中去等待其他線程執行 release操作。
  3. 當其他線程執行release操作(比如FutureTask.run()或FutureTask.cancel(…))喚醒當前線 程後,當前線程再次執行tryAcquireShared()將返回正值1,當前線程將離開線程等待隊列並喚醒它的後繼線程
  4. 最後返回計算的結果或拋出異常。

FutureTask.run()的執行過程

  1. 執行在構造函數中指定的任務(Callable.call()))。
  2. 以原子方式來更新同步狀態(調用AQS.compareAndSetState(int expect,int update),設置 state爲執行完成狀態RAN)。如果這個原子操作成功,就設置代表計算結果的變量result的值爲 Callable.call()的返回值,然後調用AQS.releaseShared(int arg)。
  3. AQS.releaseShared(int arg)首先會回調在子類Sync中實現的tryReleaseShared(arg)來執 行release操作(設置運行任務的線程runner爲null,然會返回true);AQS.releaseShared(int arg), 然後喚醒線程等待隊列中的第一個線程。
  4. 。當某個線程執行FutureTask.run()方法或FutureTask.cancel(...)方法時,會喚醒線程等待隊列的第一個線程

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章