執行緒池原理與實踐

BLLBL發表於2021-10-17

JUC的執行緒池架構

1.Executor

Executor是Java非同步任務的執行者介面,目標是執行目標任務。Executor作為執行者角色,目的是提供一種將“任務提交者”與“任務執行者”分離的機制。它只有一個函式式方法:

public interface Executor {
    void execute(Runnable command);
}

2.ExecutorService

ExecutorService繼承於Executor。它對外提供非同步任務的接收服務。ExecutorService提供了“接受非同步任務並轉交給執行者”的方法,比如submit、invoke方法等。具體如下:

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

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

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

3.AbstractExecutorService

AbstractExecutorService是一個抽象類,它實現了ExecutorService介面。AbstractExecutorService存在的目的是為ExecutorService中的介面提供預設實現。(模板模式)

4.ThreadPoolExecutor

大名鼎鼎的執行緒池實現類,繼承於AbstractExecutorService。它是核心實現類,它可以預先提供指定數量的可重用執行緒,可以對執行緒進行管理和監控。

5.ScheduledExecutorService

她繼承於ExecutorService。是一個完成延時和週期性任務的介面。

6.Executors

是一個靜態工廠類,內建的靜態工廠方法可以理解為快捷建立執行緒池的方法。

image

Executors的4種快捷建立執行緒池的方法

newSingleThreadExecutor 建立只有一個執行緒的執行緒池

newFixedThreadPool 建立固定大小的執行緒池

newCachedThreadPool 建立一個不限制執行緒數量的執行緒池,任何提交的任務都立即執行,空閒執行緒會及時回收

newScheduledThreadPool 建立一個可定期或延時執行任務的執行緒池

  • newSingleThreadExecutor
public static void main(String[] args) {
       final AtomicInteger integer = new AtomicInteger(0);

       ExecutorService pool = Executors.newSingleThreadExecutor();

       for (int i = 0; i < 5; i++) {
           pool.execute(() -> {
               System.out.println(Thread.currentThread() + " :doing" + "-" + integer.incrementAndGet());
               try {
                   Thread.sleep(500);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           });
       }

       pool.shutdown();
}

Thread[pool-1-thread-1,5,main] :doing-1
Thread[pool-1-thread-1,5,main] :doing-2
Thread[pool-1-thread-1,5,main] :doing-3
Thread[pool-1-thread-1,5,main] :doing-4
Thread[pool-1-thread-1,5,main] :doing-5

場景:任務按照提交順序,一個任務一個任務逐個執行。

以上程式碼最後呼叫shutdown來關閉執行緒池。執行shutdown方法後,執行緒池狀態變為shutdown,執行緒池將拒絕新任務,不能再往執行緒池中新增新任務。此時,執行緒池不會立刻退出,直到執行緒池中的任務處理完成後才會退出。還有一個shutdownNow方法,執行這個後,執行緒狀態變為stop,試圖停止所有正在執行的執行緒,並且不再處理阻塞佇列中等待的任務,會返回那些未執行的任務。

  • newFixedThreadPool
ExecutorService pool = Executors.newFixedThreadPool(3);

Thread[pool-1-thread-1,5,main] :doing-1
Thread[pool-1-thread-3,5,main] :doing-2
Thread[pool-1-thread-2,5,main] :doing-3
Thread[pool-1-thread-3,5,main] :doing-4
Thread[pool-1-thread-1,5,main] :doing-5

適用場景:需要任務長期執行的場景。“固定數量的執行緒池”能穩定的保證一個數,避免頻繁 回收和建立執行緒,適用於CPU密集型的任務,在CPU被執行緒長期佔用的情況下,能確保少分配執行緒。

弊端:內部使用無界佇列存放任務,當有大量任務,佇列無限增大,伺服器資源迅速耗盡。

newFixedThreadPool工廠方法返回一個ThreadPoolExecutor例項,該執行緒池例項的corePoolSize數量為引數nThread,其maximumPoolSize數量也為引數nThread,其workQueue屬性的值為LinkedBlockingQueue()無界阻塞佇列。使用Executors建立“固定數量的執行緒池”的潛在問題主要存在於其workQueue上,其值為LinkedBlockingQueue(無界阻塞佇列)。如果任務提交速度持續大於任務處理速度,就會造成佇列中大量的任務等待。如果佇列很大,很有可能導致JVM出現OOM(Out Of Memory)異常,即記憶體資源耗盡。

  • newCachedThreadPool

執行緒池內的某些執行緒無事可幹成為空閒執行緒,可以靈活回收這些空閒執行緒。

ExecutorService pool = Executors.newCachedThreadPool();

Thread[pool-1-thread-5,5,main] :doing-5
Thread[pool-1-thread-1,5,main] :doing-1
Thread[pool-1-thread-2,5,main] :doing-2
Thread[pool-1-thread-3,5,main] :doing-3
Thread[pool-1-thread-4,5,main] :doing-4

特點:在執行任務時,如果池內所有執行緒忙,則會新增新執行緒來處理。不會限制執行緒的大小,完全依賴於作業系統能夠建立的最大執行緒大小。如果存量執行緒超過了處理任務數量,就會回收執行緒。、

適用場景:快速處理突發性強、耗時短的任務場景,如Netty的NIO處理場景、REST API介面的瞬時削峰場景。

弊端:沒有最大執行緒數量限制,如果大量的非同步任務提交,伺服器資源可能耗盡。

  • newScheduledThreadPool
public static void main(String[] args) {
        final AtomicInteger integer = new AtomicInteger(0);

        ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);

        for (int i = 0; i < 5; i++) {
            pool.scheduleAtFixedRate(
                    () -> {
                        System.out.println(Thread.currentThread() + " :doing" + "-" + integer.incrementAndGet());

                    }, 0, 500, TimeUnit.MILLISECONDS);
            // 0表示首次執行任務的執行時間,500表示每次執行任務的間隔時間
        }
//        pool.shutdown();
}

因為可以週期性執行任務,所以不shutdown。

適用場景:週期性執行任務的場景。

執行緒池的標準建立方式

使用ThreadPoolExecutor構造方法建立,一個比較重要呃構造器如下:

public ThreadPoolExecutor(int corePoolSize,核心執行緒數
                              int maximumPoolSize, 最大執行緒數
                              long keepAliveTime, TimeUnit unit, 空閒時間
                              BlockingQueue<Runnable> workQueue, 阻塞佇列
                              ThreadFactory threadFactory, 執行緒工廠(執行緒產生方式)
                              RejectedExecutionHandler handler 拒絕策略) {
    ...
}

1.核心和最大執行緒數量

接收新任務時,並且當前工作執行緒池數少於核心執行緒數量,即使有工作執行緒是空閒的,它也會建立新執行緒處理任務,直到達到核心執行緒數。

2.BlockingQueue

阻塞佇列用於暫時接收任務。

3.KeepAliveTime

設定執行緒最大空閒時長,如果超過這個時間,非核心執行緒會被回收。當然,也可以呼叫allowCoreThreadTimeOut方法將超時策略應用到核心執行緒。

執行緒池的任務排程流程

  1. 工作執行緒數量小於核心執行緒數量,執行新任務時會優先建立執行緒,而不是獲取空閒執行緒。
  2. 任務數量大於核心執行緒數量,新任務將被加入阻塞佇列中。執行任務時,也是先從阻塞佇列中獲取任務。
  3. 在核心執行緒用完,阻塞佇列已滿的情況下,會建立非核心執行緒處理新任務。
  4. 在如果執行緒池總數超過maximumPoolSize,執行緒池會拒絕接收任務,為新任務執行拒絕策略。

image

ThreadFactory(執行緒工廠)

建立執行緒方式

阻塞佇列

阻塞佇列與普通度列相比:阻塞佇列為空時,會阻塞當前執行緒的元素獲取操作。當佇列中有元素,被阻塞的執行緒會被自動喚醒。

BlockingQueue是JUC包的一個超級介面,比較常用的實現類有:

(1)ArrayBlockingQueue:陣列佇列

(2)LinkedBlockingQueue:連結串列佇列

(3)PriorityBlockingQueue:優先順序佇列

(4)DelayQueue:延遲佇列

(5)SynchronousQueue:同步佇列

排程器的鉤子方法

ThreadPoolExecutor為每個任務執行前後都提供了鉤子方法。

// 任務執行之前的鉤子方法(前鉤子)
protected void beforeExecute(Thread t, Runnable r) { }
// 之後(後鉤子)
protected void afterExecute(Runnable r, Throwable t) { }
// 終止(停止鉤子)
protected void terminated() { }

beforeExecute:可用於重新初始化ThreadLocal執行緒本地變數例項、更新日誌記錄、計時統計等。

afterExecute:更新日誌記錄、計時統計等。

terminated:Executor終止時呼叫。

演示一下前鉤子。

public class TestMain {

    public static void main(String[] args) {
        final ThreadPoolExecutor pool = new ThreadPoolExecutor(
                2,
                4,
                60, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2)) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("前鉤子嗷 ~ ~ ~ ");
            }
        };

        for (int i = 0; i < 5; i++) {
            pool.execute(() -> {
                System.out.println("你誰啊");
            });
        }
    }
}

執行緒池拒絕策略

任務被拒絕有兩種情況:

  1. 執行緒池已經關閉。
  2. 工作佇列已滿且最大執行緒數已滿。

拒絕策略有以下實現:

  • AbortPolicy:拒絕策略。拋異常。
  • DiscardPolicy:拋棄策略。丟棄新來的任務。
  • DiscardOldestPolicy:拋棄最老任務策略。因為佇列是隊尾進對頭出,所以每次都是移除隊頭元素後再入隊。
  • CallerRunsPolicy:呼叫者執行策略。提交任務執行緒自己執行任務,不使用執行緒池中的執行緒。
  • 自定義策略。實現RejectExecutionHandler介面的rejectedExecution方法。

Re

《Java高併發程式設計》
(之後補原始碼)

相關文章