Java線程池學習總結

這裏很多地方引用了百度、平時收集的文章,出處找不到了

一 使用線程池的好處

池化技術相比大家已經屢見不鮮了,線程池、數據庫連接池、Http 連接池等等都是對這個思想的應用。池化技術的思想主要是爲了減少每次獲取資源的消耗,提高對資源的利用率。

線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計信息,例如已完成任務的數量。

這裏借用《Java 併發編程的藝術》提到的來說一下使用線程池的好處

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

二 Executor 框架

2.1 簡介

Executor 框架是 Java5 之後引進的,在 Java 5 之後,通過 Executor 來啓動線程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用線程池實現,節約開銷)外,還有關鍵的一點:有助於避免 this 逃逸問題。

補充:this 逃逸是指在構造函數返回之前其他線程就持有該對象的引用. 調用尚未構造完全的對象的方法可能引發令人疑惑的錯誤。

Executor 框架不僅包括了線程池的管理,還提供了線程工廠、隊列以及拒絕策略等,Executor 框架讓併發編程變得更加簡單。

2.2 Executor 框架結構(主要由三大部分組成)

1) 任務(Runnable /Callable)

執行任務需要實現的 Runnable 接口Callable接口Runnable 接口Callable 接口 實現類都可以被 ThreadPoolExecutorScheduledThreadPoolExecutor 執行。

2) 任務的執行(Executor)

如下圖所示,包括任務執行機制的核心接口 Executor ,以及繼承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutorScheduledThreadPoolExecutor 這兩個關鍵類實現了 ExecutorService 接口

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

注意: 通過查看 ScheduledThreadPoolExecutor 源代碼我們發現 ScheduledThreadPoolExecutor 實際上是繼承了 ThreadPoolExecutor 並實現了 ScheduledExecutorService ,而 ScheduledExecutorService 又實現了 ExecutorService,正如我們下面給出的類關係圖顯示的一樣。

ThreadPoolExecutor 類描述:

//AbstractExecutorService實現了ExecutorService接口
public class ThreadPoolExecutor extends AbstractExecutorService

ScheduledThreadPoolExecutor 類描述:

//ScheduledExecutorService實現了ExecutorService接口
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService

任務的執行相關接口

3) 異步計算的結果(Future)

Future 接口以及 Future 接口的實現類 FutureTask 類都可以代表異步計算的結果。

當我們把 Runnable接口Callable 接口 的實現類提交給 ThreadPoolExecutorScheduledThreadPoolExecutor 執行。(調用 submit() 方法時會返回一個 FutureTask 對象)

2.3 Executor 框架的使用示意圖

Executor 框架的使用示意圖

  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 對象)。由於 FutureTask 實現了 Runnable,我們也可以創建 FutureTask,然後直接交給 ExecutorService 執行。
  4. 最後,主線程可以執行 FutureTask.get()方法來等待任務執行完成。主線程也可以執行 FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。

三 (重要)ThreadPoolExecutor 類簡單介紹

線程池實現類 ThreadPoolExecutorExecutor 框架最核心的類。

3.1 ThreadPoolExecutor 類分析

ThreadPoolExecutor 類中提供的四個構造方法。我們來看最長的那個,其餘三個都是在這個構造方法的基礎上產生(其他幾個構造方法說白點都是給定某些默認參數的構造方法比如默認制定拒絕策略是什麼),這裏就不貼代碼講了,比較簡單。

    /**
     * 用給定的初始參數創建一個新的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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

下面這些對創建 非常重要,在後面使用線程池的過程中你一定會用到!所以,務必拿着小本本記清楚。

ThreadPoolExecutor 3 個最重要的參數:

  • corePoolSize : 核心線程數線程數定義了最小可以同時運行的線程數量。
  • maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變爲最大線程數。
  • workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,信任就會被存放在隊列中。

ThreadPoolExecutor其他常見參數:

  1. keepAliveTime:當線程池中的線程數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心線程外的線程不會立即銷燬,而是會等待,直到等待的時間超過了 keepAliveTime纔會被回收銷燬;
  2. unit : keepAliveTime 參數的時間單位。
  3. threadFactory :executor 創建新線程的時候會用到。
  4. handler :飽和策略。關於飽和策略下面單獨介紹一下。

下面這張圖可以加深你對線程池中各個參數的相互關係的理解(圖片來源:《Java性能調優實戰》):

線程池各個參數的關係

ThreadPoolExecutor 飽和策略定義:

如果當前同時運行的線程數量達到最大線程數量並且隊列也已經被放滿了任時,ThreadPoolTaskExecutor 定義一些策略:

  • ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException來拒絕新任務的處理。
  • ThreadPoolExecutor.CallerRunsPolicy:調用執行自己的線程運行任務,也就是直接在調用execute方法的線程中運行(run)被拒絕的任務,如果執行程序已關閉,則會丟棄該任務。因此這種策略會降低對於新任務提交速度,影響程序的整體性能。另外,這個策略喜歡增加隊列容量。如果您的應用程序可以承受此延遲並且你不能任務丟棄任何一個任務請求的話,你可以選擇這個策略。
  • ThreadPoolExecutor.DiscardPolicy 不處理新任務,直接丟棄掉。
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略將丟棄最早的未處理的任務請求。

舉個例子:

Spring 通過 ThreadPoolTaskExecutor 或者我們直接通過 ThreadPoolExecutor 的構造函數創建線程池的時候,當我們不指定 RejectedExecutionHandler 飽和策略的話來配置線程池的時候默認使用的是 ThreadPoolExecutor.AbortPolicy。在默認情況下,ThreadPoolExecutor 將拋出 RejectedExecutionException 來拒絕新來的任務 ,這代表你將丟失對這個任務的處理。 對於可伸縮的應用程序,建議使用 ThreadPoolExecutor.CallerRunsPolicy。當最大池被填滿時,此策略爲我們提供可伸縮隊列。(這個直接查看 ThreadPoolExecutor 的構造函數源碼就可以看出,比較簡單的原因,這裏就不貼代碼了。)

3.2 推薦使用 ThreadPoolExecutor 構造函數創建線程池

在《阿里巴巴 Java 開發手冊》“併發處理”這一章節,明確指出線程資源必須通過線程池提供,不允許在應用中自行顯示創建線程。

爲什麼呢?

使用線程池的好處是減少在創建和銷燬線程上所消耗的時間以及系統資源開銷,解決資源不足的問題。如果不使用線程池,有可能會造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。

另外《阿里巴巴 Java 開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 構造函數的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險

Executors 返回線程池對象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor : 允許請求的隊列長度爲 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允許創建的線程數量爲 Integer.MAX_VALUE ,可能會創建大量線程,從而導致 OOM。

方式一:通過ThreadPoolExecutor構造函數實現(推薦)
通過構造方法實現
方式二:通過 Executor 框架的工具類 Executors 來實現
我們可以創建三種類型的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

對應 Executors 工具類中的方法如圖所示:
通過Executor 框架的工具類Executors來實現

四 (重要)ThreadPoolExecutor 使用示例

我們上面講解了 Executor框架以及 ThreadPoolExecutor 類,下面讓我們實戰一下,來通過寫一個 ThreadPoolExecutor 的小 Demo 來回顧上面的內容。

4.1 示例代碼:Runnable+ThreadPoolExecutor

首先創建一個 Runnable 接口的實現類(當然也可以是 Callable 接口,我們上面也說了兩者的區別。)

MyRunnable.java

import java.util.Date;

/**
 * 這是一個簡單的Runnable類,需要大約5秒鐘來執行其任務。
 * @author shuang.kou
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

編寫測試程序,我們這裏以阿里巴巴推薦的使用 ThreadPoolExecutor 構造函數自定義參數的方式來創建線程池。

ThreadPoolExecutorDemo.java

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        //使用阿里巴巴推薦的創建線程池的方式
        //通過ThreadPoolExecutor構造函數自定義參數創建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //創建WorkerThread對象(WorkerThread類實現了Runnable 接口)
            Runnable worker = new MyRunnable("" + i);
            //執行Runnable
            executor.execute(worker);
        }
        //終止線程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

可以看到我們上面的代碼指定了:

  1. corePoolSize: 核心線程數爲 5。
  2. maximumPoolSize :最大線程數 10
  3. keepAliveTime : 等待時間爲 1L。
  4. unit: 等待時間的單位爲 TimeUnit.SECONDS。
  5. workQueue:任務隊列爲 ArrayBlockingQueue,並且容量爲 100;
  6. handler:飽和策略爲 CallerRunsPolicy

Output:

pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019

4.2 線程池原理分析

承接 4.1 節,我們通過代碼輸出結果可以看出:線程池每次會同時執行 5 個任務,這 5 個任務執行完之後,剩餘的 5 個任務纔會被執行。 大家可以先通過上面講解的內容,分析一下到底是咋回事?(自己獨立思考一會)

現在,我們就分析上面的輸出內容來簡單分析一下線程池原理。

**爲了搞懂線程池的原理,我們需要首先分析一下 execute方法。**在 4.1 節中的 Demo 中我們使用 executor.execute(worker)來提交一個任務到線程池中去,這個方法非常重要,下面我們來看看它的源碼:

   // 存放線程池的運行狀態 (runState) 和線程池內有效線程的數量 (workerCount)
   private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    private static int workerCountOf(int c) {
        return c & CAPACITY;
    }

    private final BlockingQueue<Runnable> workQueue;

    public void execute(Runnable command) {
        // 如果任務爲null,則拋出異常。
        if (command == null)
            throw new NullPointerException();
        // ctl 中保存的線程池當前的一些狀態信息
        int c = ctl.get();

        //  下面會涉及到 3 步 操作
        // 1.首先判斷當前線程池中之行的任務數量是否小於 corePoolSize
        // 如果小於的話,通過addWorker(command, true)新建一個線程,並將任務(command)添加到該線程中;然後,啓動該線程從而執行任務。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2.如果當前之行的任務數量大於等於 corePoolSize 的時候就會走到這裏
        // 通過 isRunning 方法判斷線程池狀態,線程池處於 RUNNING 狀態纔會被並且隊列可以加入任務,該任務纔會被加入進去
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 再次獲取線程池狀態,如果線程池狀態不是 RUNNING 狀態就需要從任務隊列中移除任務,並嘗試判斷線程是否全部執行完畢。同時執行拒絕策略。
            if (!isRunning(recheck) && remove(command))
                reject(command);
                // 如果當前線程池爲空就新創建一個線程並執行。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //3. 通過addWorker(command, false)新建一個線程,並將任務(command)添加到該線程中;然後,啓動該線程從而執行任務。
        //如果addWorker(command, false)執行失敗,則通過reject()執行相應的拒絕策略的內容。
        else if (!addWorker(command, false))
            reject(command);
    }

通過下圖可以更好的對上面這 3 步做一個展示,下圖是我爲了省事直接從網上找到,原地址不明。

圖解線程池實現原理

現在,讓我們在回到 4.1 節我們寫的 Demo, 現在應該是不是很容易就可以搞懂它的原理了呢?

沒搞懂的話,也沒關係,可以看看我的分析:

我們在代碼中模擬了 10 個任務,我們配置的核心線程數爲 5 、等待隊列容量爲 100 ,所以每次只可能存在 5 個任務同時執行,剩下的 5 個任務會被放到等待隊列中去。當前的 5 個任務之行完成後,纔會之行剩下的 5 個任務。

4.3 幾個常見的對比

4.3.1 Runnable vs Callable

Runnable自 Java 1.0 以來一直存在,但Callable僅在 Java 1.5 中引入,目的就是爲了來處理Runnable不支持的用例。Runnable 接口不會返回結果或拋出檢查異常,但是**Callable 接口**可以。所以,如果任務不需要返回結果或拋出異常推薦使用 Runnable 接口,這樣代碼看起來會更加簡潔。

工具類 Executors 可以實現 Runnable 對象和 Callable 對象之間的相互轉換。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。

Runnable.java

@FunctionalInterface
public interface Runnable {
   /**
    * 被線程執行,沒有返回值也無法拋出異常
    */
    public abstract void run();
}

Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * 計算結果,或在無法這樣做時拋出異常。
     * @return 計算得出的結果
     * @throws 如果無法計算結果,則拋出異常
     */
    V call() throws Exception;
}

4.3.2 execute() vs submit()

  1. execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否;
  2. submit()方法用於提交需要返回值的任務。線程池會返回一個 Future 類型的對象,通過這個 Future 對象可以判斷任務是否執行成功,並且可以通過 Futureget()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

我們以**AbstractExecutorService**接口中的一個 submit 方法爲例子來看看源代碼:

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

上面方法調用的 newTaskFor 方法返回了一個 FutureTask 對象。

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

我們再來看看execute()方法:

    public void execute(Runnable command) {
      ...
    }

4.3.3 shutdown()VSshutdownNow()

  • shutdown() :關閉線程池,線程池的狀態變爲 SHUTDOWN。線程池不再接受新任務了,但是隊列裏的任務得執行完畢。
  • shutdownNow() :關閉線程池,線程的狀態變爲 STOP。線程池會終止當前正在運行的任務,並停止處理排隊的任務並返回正在等待執行的 List。

4.3.2 isTerminated() VS isShutdown()

  • isShutDown 當調用 shutdown() 方法後返回爲 true。
  • isTerminated 當調用 shutdown() 方法後,並且所有提交的任務完成後返回爲 true

4.4 加餐:Callable+ThreadPoolExecutor示例代碼

MyCallable.java


import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        //返回執行當前 Callable 的線程名字
        return Thread.currentThread().getName();
    }
}

CallableDemo.java


import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CallableDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {

        //使用阿里巴巴推薦的創建線程池的方式
        //通過ThreadPoolExecutor構造函數自定義參數創建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        List<Future<String>> futureList = new ArrayList<>();
        Callable<String> callable = new MyCallable();
        for (int i = 0; i < 10; i++) {
            //提交任務到線程池
            Future<String> future = executor.submit(callable);
            //將返回值 future 添加到 list,我們可以通過 future 獲得 執行 Callable 得到的返回值
            futureList.add(future);
        }
        for (Future<String> fut : futureList) {
            try {
                System.out.println(new Date() + "::" + fut.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //關閉線程池
        executor.shutdown();
    }
}

Output:

Wed Nov 13 13:40:41 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-5
Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-2
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-1
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-4
Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5

五 幾種常見的線程池詳解

5.1 FixedThreadPool

5.1.1 介紹

FixedThreadPool 被稱爲可重用固定線程數的線程池。通過 Executors 類中的相關源代碼來看一下相關實現:

   /**
     * 創建一個可重用固定數量線程的線程池
     */
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

另外還有一個 FixedThreadPool 的實現方法,和上面的類似,所以這裏不多做闡述:

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

從上面源代碼可以看出新創建的 FixedThreadPoolcorePoolSizemaximumPoolSize 都被設置爲 nThreads,這個 nThreads 參數是我們使用的時候自己傳遞的。

5.1.2 執行任務過程介紹

FixedThreadPoolexecute() 方法運行示意圖(該圖片來源:《Java 併發編程的藝術》):

FixedThreadPool的execute()方法運行示意圖

上圖說明:

  1. 如果當前運行的線程數小於 corePoolSize, 如果再來新任務的話,就創建新的線程來執行任務;
  2. 當前運行的線程數等於 corePoolSize 後, 如果再來新任務的話,會將任務加入 LinkedBlockingQueue
  3. 線程池中的線程執行完 手頭的任務後,會在循環中反覆從 LinkedBlockingQueue 中獲取任務來執行;

5.1.3 爲什麼不推薦使用FixedThreadPool

FixedThreadPool 使用無界隊列 LinkedBlockingQueue(隊列的容量爲 Intger.MAX_VALUE)作爲線程池的工作隊列會對線程池帶來如下影響 :

  1. 當線程池中的線程數達到 corePoolSize 後,新任務將在無界隊列中等待,因此線程池中的線程數不會超過 corePoolSize;
  2. 由於使用無界隊列時 maximumPoolSize 將是一個無效參數,因爲不可能存在任務隊列滿的情況。所以,通過創建 FixedThreadPool的源碼可以看出創建的 FixedThreadPoolcorePoolSizemaximumPoolSize 被設置爲同一個值。
  3. 由於 1 和 2,使用無界隊列時 keepAliveTime 將是一個無效參數;
  4. 運行中的 FixedThreadPool(未執行 shutdown()shutdownNow())不會拒絕任務,在任務比較多的時候會導致 OOM(內存溢出)。

5.2 SingleThreadExecutor 詳解

5.2.1 介紹

SingleThreadExecutor 是隻有一個線程的線程池。下面看看SingleThreadExecutor 的實現:

   /**
     *返回只有一個線程的線程池
     */
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
   public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

從上面源代碼可以看出新創建的 SingleThreadExecutorcorePoolSizemaximumPoolSize 都被設置爲 1.其他參數和 FixedThreadPool 相同。

5.2.2 執行任務過程介紹

SingleThreadExecutor 的運行示意圖(該圖片來源:《Java 併發編程的藝術》):
SingleThreadExecutor的運行示意圖

上圖說明;

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

5.2.3 爲什麼不推薦使用SingleThreadExecutor

SingleThreadExecutor 使用無界隊列 LinkedBlockingQueue 作爲線程池的工作隊列(隊列的容量爲 Intger.MAX_VALUE)。SingleThreadExecutor 使用無界隊列作爲線程池的工作隊列會對線程池帶來的影響與 FixedThreadPool 相同。說簡單點就是可能會導致 OOM,

5.3 CachedThreadPool 詳解

5.3.1 介紹

CachedThreadPool 是一個會根據需要創建新線程的線程池。下面通過源碼來看看 CachedThreadPool 的實現:

    /**
     * 創建一個線程池,根據需要創建新線程,但會在先前構建的線程可用時重用它。
     */
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

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

CachedThreadPoolcorePoolSize 被設置爲空(0),maximumPoolSize被設置爲 Integer.MAX.VALUE,即它是無界的,這也就意味着如果主線程提交任務的速度高於 maximumPool 中線程處理任務的速度時,CachedThreadPool 會不斷創建新的線程。極端情況下,這樣會導致耗盡 cpu 和內存資源。

5.3.2 執行任務過程介紹

CachedThreadPool 的 execute()方法的執行示意圖(該圖片來源:《Java 併發編程的藝術》):
CachedThreadPool的execute()方法的執行示意圖

上圖說明:

  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 方法執行完成;

5.3.3 爲什麼不推薦使用CachedThreadPool

CachedThreadPool允許創建的線程數量爲 Integer.MAX_VALUE ,可能會創建大量線程,從而導致 OOM。

六 ScheduledThreadPoolExecutor 詳解

ScheduledThreadPoolExecutor 主要用來在給定的延遲後運行任務,或者定期執行任務。 這個在實際項目中基本不會被用到,所以對這部分大家只需要簡單瞭解一下它的思想。關於如何在Spring Boot 中 實現定時任務,可以查看這篇文章《5分鐘搞懂如何在Spring Boot中Schedule Tasks》

6.1 簡介

ScheduledThreadPoolExecutor 使用的任務隊列 DelayQueue 封裝了一個 PriorityQueuePriorityQueue 會對隊列中的任務進行排序,執行所需時間短的放在前面先被執行(ScheduledFutureTasktime 變量小的先執行),如果執行所需時間相同則先提交的任務將被先執行(ScheduledFutureTasksquenceNumber 變量小的先執行)。

ScheduledThreadPoolExecutorTimer 的比較:

  • Timer 對系統時鐘的變化敏感,ScheduledThreadPoolExecutor不是;
  • Timer 只有一個執行線程,因此長時間運行的任務可以延遲其他任務。 ScheduledThreadPoolExecutor 可以配置任意數量的線程。 此外,如果你想(通過提供 ThreadFactory),你可以完全控制創建的線程;
  • TimerTask 中拋出的運行時異常會殺死一個線程,從而導致 Timer 死機:-( …即計劃任務將不再運行。ScheduledThreadExecutor 不僅捕獲運行時異常,還允許您在需要時處理它們(通過重寫 afterExecute 方法ThreadPoolExecutor)。拋出異常的任務將被取消,但其他任務將繼續運行。

綜上,在 JDK1.5 之後,你沒有理由再使用 Timer 進行任務調度了。

備註: Quartz 是一個由 java 編寫的任務調度庫,由 OpenSymphony 組織開源出來。在實際項目開發中使用 Quartz 的還是居多,比較推薦使用 Quartz。因爲 Quartz 理論上能夠同時對上萬個任務進行調度,擁有豐富的功能特性,包括任務調度、任務持久化、可集羣化、插件等等。

6.2 運行機制

ScheduledThreadPoolExecutor運行機制

ScheduledThreadPoolExecutor 的執行主要分爲兩大部分:

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

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

  • 使用 DelayQueue 作爲任務隊列;
  • 獲取任務的方不同
  • 執行週期任務後,增加了額外的處理

6.3 ScheduledThreadPoolExecutor 執行週期任務的步驟

ScheduledThreadPoolExecutor執行週期任務的步驟

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

七 線程池大小確定

線程池數量的確定一直是困擾着程序員的一個難題,大部分程序員在設定線程池大小的時候就是隨心而定。我們並沒有考慮過這樣大小的配置是否會帶來什麼問題,我自己就是這大部分程序員中的一個代表。

由於筆主對如何確定線程池大小也沒有什麼實際經驗,所以,這部分內容參考了網上很多文章/書籍。

首先,可以肯定的一點是線程池大小設置過大或者過小都會有問題。合適的纔是最好,貌似在 95 % 的場景下都是合適的。

如果閱讀過我的上一篇關於線程池的文章的話,你一定知道:

如果我們設置的線程池數量太小的話,如果同一時間有大量任務/請求需要處理,可能會導致大量的請求/任務在任務隊列中排隊等待執行,甚至會出現任務隊列滿了之後任務/請求無法處理的情況,或者大量任務堆積在任務隊列導致 OOM。這樣很明顯是有問題的! CPU 根本沒有得到充分利用。

但是,如果我們設置線程數量太大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換,從而增加線程的執行時間,影響了整體執行效率。

上下文切換:

多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能得到有效執行,CPU 採取的策略是爲每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換回這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

有一個簡單並且適用面比較廣的公式:

  • CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置爲 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是爲了防止線程偶發的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處於空閒狀態,而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閒時間。
  • I/O 密集型任務(2N): 這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。

八 參考

九 其他推薦閱讀

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