Java併發程式設計——深入理解執行緒池

it_was發表於2020-09-25

執行緒池是指管理一組同構工作執行緒的資源池。

1.1使用執行緒池的好處:boom:

  • 降低資源消耗。透過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

1.2 Executor框架

首先看下Executor原始碼,即Executor是一個核心的基礎介面,其中只有execute方法

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

1.3 Executor框架三大組成部分

  • 任務(Runnable /Callable)
    執行任務需要實現的 Runnable 介面Callable介面

  • 任務的執行(Executor)
    任務執行機制的核心介面 Executor ,以及繼承自 Executor 介面的 ExecutorService 介面。ThreadPoolExecutorScheduledThreadPoolExecutor 這兩個關鍵類實現了 ExecutorService 介面

這裡提了很多底層的類關係,但是,實際上我們需要更多關注的是 ThreadPoolExecutor 這個類,這個類在我們實際使用執行緒池的過程中,使用頻率還是非常高的。

任務的執行相關介面

  • 非同步計算的結果(Future)

Future 介面以及 Future 介面的實現類 FutureTask 類都可以代表非同步計算的結果。
當我們把 Runnable介面Callable 介面 的實現類提交給 ThreadPoolExecutorScheduledThreadPoolExecutor 執行。呼叫 submit() 方法時會返回一個 FutureTask 物件

1.4 Executor框架的使用

Java併發程式設計——深入理解執行緒池

  1. 主執行緒首先要建立實現 Runnable 或者 Callable 介面的任務物件。
  2. 把建立完成的實現 Runnable/Callable介面的 物件直接交給 ExecutorService 執行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 物件或Callable 物件提交給 ExecutorService 執行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable <T> task))。
  3. 如果執行 ExecutorService.submit(…)ExecutorService 將返回一個實現Future介面的物件(我們剛剛也提到過了執行 execute()方法和 submit()方法的區別,submit()會返回一個 `FutureTask 物件)。
  4. 主執行緒可以執行 FutureTask.get()方法來等待任務執行完成。
    主執行緒也可以執行 FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。

1.5 ThreadPoolExecutor 類分析

執行緒池實現類 ThreadPoolExecutor 是 Executor 框架最核心的類。
話不多說,先看下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;
    }

:boom:ThreadPoolExecutor 3 個最重要的引數:boom:

:one: corePoolSize : 核心執行緒數執行緒數定義了最小可以同時執行的執行緒數量。
:two: maximumPoolSize : 當佇列中存放的任務達到佇列容量的時候,當前可以同時執行的執行緒數量變為最大執行緒數。
:three: workQueue: 當新任務來的時候會先判斷當前執行的執行緒數量是否達到核心執行緒數,如果達到的話,新任務就會被存放在佇列中。

ThreadPoolExecutor其他常見引數:

  • keepAliveTime:當執行緒池中的執行緒數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心執行緒外的執行緒不會立即銷燬,而是會等待,直到等待的時間超過了 keepAliveTime才會被回收銷燬;
  • unit : keepAliveTime 引數的時間單位。
  • threadFactory :executor 建立新執行緒的時候會用到。
  • handler :飽和策略。:boom:

    ThreadPoolExecutor 飽和策略定義:如果當前同時執行的執行緒數量達到最大執行緒數量並且佇列也已經被放滿了任務時,ThreadPoolTaskExecutor 定義一些策略:

    1. ThreadPoolExecutor.AbortPolicy:丟擲 RejectedExecutionException來拒絕新任務的處理。這是預設的拒絕策略。
    2. ThreadPoolExecutor.CallerRunsPolicy:呼叫執行自己的執行緒執行任務,也就是直接在呼叫execute方法的執行緒中執行(run)被拒絕的任務,如果執行程式已關閉,則會丟棄該任務。因此這種策略會降低對於新任務提交速度,影響程式的整體效能。如果您的應用程式可以承受此延遲並且你要求任何一個任務請求都要被執行的話,你可以選擇這個策略。
    3. ThreadPoolExecutor.DiscardPolicy: 不處理新任務,直接丟棄掉。
    4. ThreadPoolExecutor.DiscardOldestPolicy: 此策略將丟棄最早的未處理的任務請求。

透過以下圖片理解上述引數

Java併發程式設計——深入理解執行緒池

1.6 執行緒池建立方法

1. 透過使用ThreadPoolExecutor的建構函式建立!:+1:
Java併發程式設計——深入理解執行緒池
2. 透過使用Executors的靜態工廠方法建立!:-1:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
 public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

透過以上靜態工廠方法我們可以發現不同的ThreadPoolExecutor有各自的缺點:

  • FixedThreadPool 和 SingleThreadExecutor : 允許請求的佇列長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允許建立的執行緒數量為 Integer.MAX_VALUE ,可能會建立大量執行緒,從而導致 OOM。

ThreadPoolExecutor類執行流程:boom:
Java併發程式設計——深入理解執行緒池

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章