Java 執行緒 Executor 框架詳解與使用

孫_悟_空發表於2017-05-28

在HotSpot VM的執行緒模型中,Java執行緒被一對一對映為本地作業系統執行緒。Java執行緒啟動時會建立一個本地作業系統執行緒;當該Java執行緒終止時,這個作業系統執行緒也會被回收,在JVM中我們可以通過-Xss設定每個執行緒的大小。作業系統會排程所有執行緒並將它們分配給可用的CPU。

在上層,java多執行緒程式通常把應用分解為若干個任務,然後使用使用者級的排程器(Executor框架)將這些任務對映為固定數量的執行緒;在底層,作業系統核心將這些執行緒對映到硬體處理器上。這種兩級排程模型的示意圖如下圖所示

這裡寫圖片描述

通過上圖可以看出應用程式通過Executor控制上層排程,作業系統核心控制下層排程。
注:oskernel作業系統核心包括作業系統軟體和應用,只是作業系統最基本的功能,例如記憶體管理,程式管理,硬體驅動等

Executor結構

executor結構主要包括任務、任務的執行和非同步結果的計算。

任務

包括被執行任務需要實現的介面:Runnable介面或Callable介面

任務的執行

包括任務執行機制的核心介面Executor,以及繼承自Executor的ExecutorService介面。Executor框架有兩個關鍵類實現了ExecutorService介面(ThreadPoolExecutor和ScheduledThreadPoolExecutor)

非同步計算的結果

包括介面Future和實現Future介面的FutureTask類

下面我們來看看executor類圖

這裡寫圖片描述

在Executor使用過程中,主執行緒首先要建立實現Runnable或者Callable介面的任務物件。工具類Executors可以把一個Runnable物件封裝為一個Callable物件(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。

如果執行ExecutorService.submit(…),ExecutorService將返回一個實現Future介面的物件(FutureTask)。由於FutureTask實現了Runnable,我們也可以建立FutureTask,然後直接交給ExecutorService執行。最後,主執行緒可以執行FutureTask.get()方法來等待任務執行完成。主執行緒也可以執行FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。

FixedThreadPool

初始化

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

FixedThreadPool的corePoolSize和maximumPoolSize都被設定為建立FixedThreadPool時指定的引數nThreads。當執行緒池中的執行緒數大於corePoolSize時,keepAliveTime為多餘的空閒執行緒等待新任務的最長時間,超過這個時間後多餘的執行緒將被終止。這裡把keepAliveTime設定為0L,意味著多餘的空閒執行緒會被立即終止

執行過程

下面是FixedThreadPool執行過程示意圖

這裡寫圖片描述

  • 1、如果當前執行的執行緒數少於corePoolSize,則建立新執行緒來執行任務。
  • 2、線上程池完成預熱之後(當前執行的執行緒數等於corePoolSize),將任務加入LinkedBlockingQueue。
  • 3、執行緒執行完1中的任務後,會在迴圈中反覆從LinkedBlockingQueue獲取任務來執行。

FixedThreadPool使用無界佇列LinkedBlockingQueue作為執行緒池的工作佇列(佇列的容量為Integer.MAX_VALUE)。使用無界佇列作為工作佇列會對執行緒池帶來如下影響

  • 1、當執行緒池中的執行緒數達到corePoolSize後,新任務將在無界佇列中等待,因此執行緒池中的執行緒數不會超過corePoolSize。
  • 2、由於1,使用無界佇列時maximumPoolSize將是一個無效引數。
  • 3、由於1和2,使用無界佇列時keepAliveTime將是一個無效引數。
  • 4、由於使用無界佇列,執行中的FixedThreadPool(未執行方法shutdown()或shutdownNow())不會拒絕任務(不會呼叫RejectedExecutionHandler.rejectedExecution方法)。

使用場景

FixedThreadPool適用於為了滿足資源管理的需求,而需要限制當前執行緒數量的應用場景,它適用於負載比較重的伺服器

SingleThreadExecutor

初始化

建立使用單個執行緒的SingleThread-Executor

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

SingleThreadExecutor的corePoolSize和maximumPoolSize被設定為1。其他引數與FixedThreadPool相同。SingleThreadExecutor使用無界佇列LinkedBlockingQueue作為執行緒池的工作佇列(佇列的容量為Integer.MAX_VALUE)。SingleThreadExecutor使用無界佇列作為工作佇列對執行緒池帶來的影響與FixedThreadPool相同

執行過程

下圖是SingleThreadExecutor的執行過程示意圖

這裡寫圖片描述

  • 1、如果當前執行的執行緒數少於corePoolSize(即執行緒池中無執行的執行緒),則建立一個新執行緒來執行任務。
  • 2、線上程池完成預熱之後(當前執行緒池中有一個執行的執行緒),將任務加入LinkedBlockingQueue。
  • 3、執行緒執行完1中的任務後,會在一個無限迴圈中反覆從LinkedBlockingQueue獲取任務來執行。

使用場景

SingleThreadExecutor適用於需要保證順序地執行各個任務;並且在任意時間點,不會有多個執行緒是活動的應用場景。

CachedThreadPool

初始化

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

CachedThreadPool的corePoolSize被設定為0,即corePool為空;maximumPoolSize被設定為Integer.MAX_VALUE,即maximumPool是無界的。這裡把keepAliveTime設定為60L,意味著CachedThreadPool中的空閒執行緒等待新任務的最長時間為60秒,空閒執行緒超過60秒後將會被終止。

FixedThreadPool和SingleThreadExecutor使用無界佇列LinkedBlockingQueue作為執行緒池的工作佇列。CachedThreadPool使用沒有容量的SynchronousQueue作為執行緒池的工作佇列,但CachedThreadPool的maximumPool是無界的。這意味著,如果主執行緒提交任務的速度高於maximumPool中執行緒處理任務的速度時,CachedThreadPool會不斷建立新執行緒。極端情況下,CachedThreadPool會因為建立過多執行緒而耗盡CPU和記憶體資源。

執行過程

下圖是CachedThreadPool的執行過程示意圖

這裡寫圖片描述

1、首先執行SynchronousQueue.offer(Runnable task)。如果當前maximumPool中有空閒執行緒正在執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那麼主執行緒執行offer操作與空閒執行緒執行的poll操作配對成功,主執行緒把任務交給空閒執行緒執行,execute()方法執行完成;否則執行下面的步驟2)。

2、當初始maximumPool為空,或者maximumPool中當前沒有空閒執行緒時,將沒有執行緒執行SynchronousQueue.poll
(keepAliveTime,TimeUnit.NANOSECONDS)。這種情況下,步驟1)將失敗。此時CachedThreadPool會建立一個新執行緒執行任務,execute()方法執行完成。

3、在步驟2)中新建立的執行緒將任務執行完後,會執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這個poll操作會讓空閒執行緒最多在SynchronousQueue中等待60秒鐘。如果60秒鐘內主執行緒提交了一個新任務(主執行緒執行步驟1)),那麼這個空閒執行緒將執行主執行緒提交的新任務;否則,這個空閒執行緒將終止。由於空閒60秒的空閒執行緒會被終止,因此長時間保持空閒的CachedThreadPool不會使用任何資源。

前面提到過,SynchronousQueue是一個沒有容量的阻塞佇列。每個插入操作必須等待另一個執行緒的對應移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主執行緒提交的任務傳遞給空閒執行緒執行。CachedThreadPool中任務傳遞的示意圖如下圖所示:

這裡寫圖片描述

使用場景

看名字我們可以知道cached快取,CachedThreadPool可以建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們,對於執行很多短期非同步任務的程式而言,這些執行緒池通常可提高程式效能。呼叫 execute 將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。

CachedThreadPool是大小無界的執行緒池,適用於執行很多的短期非同步任務的小程式,或者是負載較輕的伺服器。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor。它主要用來在給定的延遲之後執行任務,或者定期執行任務。ScheduledThreadPoolExecutor的功能與Timer類似,但ScheduledThreadPoolExecutor功能更強大、更靈活。Timer對應的是單個後臺執行緒,而ScheduledThreadPoolExecutor可以在建構函式中指定多個對應的後臺執行緒數

初始化

ScheduledThreadPoolExecutor通常使用工廠類Executors來建立。Executors可以建立2種型別的ScheduledThreadPoolExecutor,如下。

ScheduledThreadPoolExecutor:包含若干個執行緒的ScheduledThreadPoolExecutor。
SingleThreadScheduledExecutor:只包含一個執行緒的ScheduledThreadPoolExecutor。

下面分別介紹這兩種ScheduledThreadPoolExecutor。

public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }
 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue());
    }

直接呼叫父類ThreadPoolExecutor構造方法進行初始化。ScheduledThreadPoolExecutor適用於需要多個後臺執行緒執行週期任務,同時為了滿足資源管理的需求而需要限制後臺執行緒的數量的應用場景。

下面看看如何建立SingleThreadScheduledExecutor

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
  public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1, threadFactory));
    }

執行過程

DelayQueue是一個無界佇列,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中沒有什麼意義(設定maximumPoolSize的大小沒有什麼效果)。ScheduledThreadPoolExecutor的執行主要分為兩大部分。

1、當呼叫ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法時,會向ScheduledThreadPoolExecutor的DelayQueue新增一個實現了RunnableScheduledFutur介面的ScheduledFutureTask。
2、執行緒池中的執行緒從DelayQueue中獲取ScheduledFutureTask,然後執行任務。

下面看看ScheduedThreadPoolExecutor執行過程示意圖

這裡寫圖片描述

ScheduledThreadPoolExecutor為了實現週期性的執行任務,對ThreadPoolExecutor做了如下
的修改。

  • 1、使用DelayQueue作為任務佇列。
  • 2、獲取任務的方式不同(後文會說明)。
  • 3、執行週期任務後,增加了額外的處理(後文會說明)。

實現過程分析

ScheduledThreadPoolExecutor會把待排程的任務(ScheduledFutureTask)放到一個DelayQueue中。ScheduledFutureTask主要包含3個成員變數,如下。

  • 1、long time,表示這個任務將要被執行的具體時間。
  • 2、long sequenceNumber,表示這個任務被新增到ScheduledThreadPoolExecutor中的序號。
  • 3、long period,表示任務執行的間隔週期。

DelayQueue封裝了一個PriorityQueue,這個PriorityQueue會對佇列中的ScheduledFutureTask進行排序。排序時,time小的排在前面(時間早的任務將被先執行)。如果兩個ScheduledFutureTask的time相同,就比較sequenceNumber,sequenceNumber小的排在前面(也就是說,如果兩個任務的執行時間相同,那麼先提交的任務將被先執行)。首先,讓我們看看ScheduledThreadPoolExecutor中的執行緒執行週期任務的過程。如下圖所示

這裡寫圖片描述

  • 1、執行緒1從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大於等於當前時間。
  • 2、執行緒1執行這個ScheduledFutureTask。
  • 3、執行緒1修改ScheduledFutureTask的time變數為下次將要被執行的時間。
  • 4、執行緒1把這個修改time之後的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

下面我們看看DelayQueue.take()的原始碼是如何實現的

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.1
                } else {
                    long delay = first.getDelay(TimeUnit.NANOSECONDS);
                    if (delay > 0) {
                        long tl = available.awaitNanos(delay); // 2.2
                    } else {
                        E x = q.poll(); // 2.3.1
                        assert x != null;
                        if (q.size() != 0)
                            available.signalAll(); // 2.3.2
                        return x;
                    }
                }
            }
        } finally {
            lock.unlock(); // 3
        }
    }

下面我們對上面的程式碼用流程圖展示出來

這裡寫圖片描述

1、獲取Lock。
2、獲取週期任務。

  • a、如果PriorityQueue為空,當前執行緒到Condition中等待;否則執行下面的2.2。
  • b、如果PriorityQueue的頭元素的time時間比當前時間大,到Condition中等待到time時間;否則執行下面的2.3。
  • c、獲取PriorityQueue的頭元素(2.3.1);如果PriorityQueue不為空,則喚醒在Condition中等待的所有執行緒(2.3.2)。

3、釋放Lock。

ScheduledThreadPoolExecutor在一個迴圈中執行步驟2,直到執行緒從PriorityQueue獲取到一個元素之後(執行2.3.1之後),才會退出無限迴圈(結束步驟2)。

下面我來看看DelayQueue.add()原始碼實現

   public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock(); // 1
        try {
            E first = q.peek();
            q.offer(e); // 2.1
            if (first == null || e.compareTo(first) < 0)
                available.signalAll(); // 2.2
            return true;
        } finally {
            lock.unlock(); // 3
        }
    }

下面我們對上面的程式碼用流程圖展示出來

這裡寫圖片描述

1、獲取Lock。
2、新增任務。

  • a、向PriorityQueue新增任務。
  • b、如果在上面2.1中新增的任務是PriorityQueue的頭元素,喚醒在Condition中等待的所有執行緒。

3、釋放Lock。

使用場景

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

相關文章