一文掌握線程池實現原理
線程池簡介
Java 在使用線程執行程序時,需要調用操作系統內核的 API 創建一個內核線程,操作系統要爲線程分配一系列的資源;當該 Java 線程被終止時,對應的內核線程也會被回收。因此,頻繁的創建和銷燬線程需要消耗大量資源。此外,由於 CPU 核數有限,大量的線程上下文切換會增加系統的性能開銷,無限制地創建線程還可能導致內存溢出。爲此,Java 在 JDK1.5 版本中引入了線程池。
線程池是一種重用線程的機制,用於提高線程的利用率和管理線程的生命週期,常用於多線程編程和異步編程。
線程池的優點:
-
降低資源消耗:線程池中的線程可以重複使用,避免因頻繁創建和銷燬線程而帶來的性能開銷。
-
提高響應速度:向線程池中提交任務時,無需創建線程即可執行任務處理。
-
方便線程管理:線程池可以對其中的線程進行統一管理、監控,避免因大量創建線程而導致內存溢出。
線程池實現原理
Java 線程池的核心實現類爲 ThreadPoolExecutor。
ThreadPoolExecutor 依賴關係
ThreadPoolExecutor 依賴關係圖:
其中:
-
Executor(接口):該接口線程池處理任務的頂級接口,定義了一個用於執行任務的方法 execute(Runnable command)。其中參數 command 爲實現了 Runnable 或 Callable 接口的 Task 任務。
-
ExecutorService(接口):該接口繼承了 Executor 接口,它擴展了 Executor 接口,並添加了一些管理線程池的方法(如:提交任務、關閉線程池等)。
-
AbstractExecutorService(抽象類):該類實現了 ExecutorService 接口。
構造函數
如 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.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;
}
核心參數:
-
corePoolSize:線程池中的核心線程數,當向線程池中提交任務時,線程池創建新線程執行任務處理,直到當前線程數達到 corePoolSize 設定的值。此時提交的新任務會被添加到阻塞隊列中,等待其他線程執行完成。默認情況下,空閒的核心線程並不會被回收(即:屬性 allowCoreThreadTimeOut 的值默認爲 false),如果需要回收空閒的核心線程,設置屬性 allowCoreThreadTimeOut 的值爲 true 即可。
-
maximumPoolSize:線程池中的最大線程數,當線程池中的線程數達到 corePoolSize 設定的值且核心線程均被佔用時,如果阻塞隊列已滿並向線程池中繼續提交任務,則創建新的線程執行任務處理,直到當前線程數達到 maximumPoolSize 設定的值。
-
keepAliveTime:線程池中線程的存活時間,該參數只會在線程數大於 corePoolSize 設定的值時才生效,即:非核心線程的空閒時間超過 keepAliveTime 設定的值時會被回收。
-
unit:線程池中參數 keepAliveTime 的時間單位,默認爲 TimeUnit.MILLISECONDS(毫秒),其他時間單位:
-
TimeUnit.NANOSECONDS(納秒)
-
TimeUnit.MICROSECONDS(微秒)
-
TimeUnit.MILLISECONDS(毫秒)
-
TimeUnit.SECONDS(秒)
-
TimeUnit.MINUTES(分鐘)
-
TimeUnit.HOURS(小時)
-
TimeUnit.DAYS(天)
-
workQueue:線程池中保存任務的阻塞隊列,當線程池中的線程數達到 corePoolSize 設定的值,繼續提交任務時會將任務添加到阻塞隊列中等待。默認爲 LinkedBlockingQueue,可選擇的阻塞隊列:
-
LinkedBlockingQueue(基於鏈表實現的阻塞隊列)。
-
ArrayBlockingQueue(基於數組實現的阻塞隊列)。
-
SynchronousQueue(只有一個元素的阻塞隊列)。
-
PriorityBlockingQueue(實現了優先級的阻塞隊列)。
-
DelayQueue(實現了延遲功能的阻塞隊列)。
-
threadFactory:線程池中創建線程的工廠,可以通過自定義線程工廠的方式爲線程設置一個便於識別的線程名,默認爲 DefaultThreadFactory。
-
handler:線程池的拒絕策略(又稱飽和策略),當線程池中的線程數達到 maximumPoolSize 設定的值且阻塞隊列已滿時,繼續向線程池中提交任務,就會觸發拒絕策略,默認爲 AbortPolicy。可選的拒絕策略:
-
AbortPolicy:丟棄任務,並拋出 RejectedExecutionException 異常。
-
DiscardPolicy:丟棄任務,不拋出異常。
-
DiscardOldestPolicy:丟棄隊列中最早的未處理的任務,執行當前任務。
-
CallerRunsPolicy:由調用者所在的線程來執行任務。
線程池生命週期
在 ThreadPoolExecutor 類中定義了線程池的五種狀態。
源碼如下:
// ctl共32位,其中高3位表示線程池運行狀態,低29位表示線程池中的線程數量。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
// 二進制爲0001 1111 1111 1111 1111 1111 1111 1111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
// RUNNING狀態:二進制爲111 00000000000000000000000000000
private static final int RUNNING = -1 << COUNT_BITS;
// SHUTDOWN狀態:二進制爲000 00000000000000000000000000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// STOP狀態:二進制爲001 00000000000000000000000000000
private static final int STOP = 1 << COUNT_BITS;
// TIDYING狀態:二進制爲010 00000000000000000000000000000
private static final int TIDYING = 2 << COUNT_BITS;
// TERMINATED狀態:二進制011 00000000000000000000000000000
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 線程池中的線程數量
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
其中:
ctl 是一個 AtomicInteger 類型的變量:
-
高 3 位表示線程池的運行狀態。
-
低 29 位表示線程池中的線程數量。
線程池運行狀態:
-
RUNNING:處於 RUNNING 狀態的線程池會接受新任務提交,同時也會處理任務隊列中的任務。
-
SHUTDOWN:處於 SHUTDOWN 狀態的線程池不會接受新任務提交,但是會處理任務隊列中的任務。
-
STOP:處於 STOP 狀態的線程池不會接受新任務提交,也不會處理任務隊列中的任務,並且會中斷正在執行的任務。
-
TIDYING:當所有的任務已終止、workerCount 數量爲 0 時,線程池會進入 TIDYING 狀態,進入 TIDYING 狀態會執行鉤子方法 terminated()。
-
TERMINATED:執行完鉤子方法 terminated() 後進入 TERMINATED 狀態,此時線程池已終止。
線程池狀態轉換,如圖所示:
工作線程
Worker(工作線程)是 ThreadPoolExecutor 的一個內部類,它繼承了 AbstractQueuedSynchronizer(即:AQS)並實現了 Runnable 接口。其中:
-
通過繼承 AbstractQueuedSynchronizer 類,可以控制在主線程中調用 shutdown() 方法時不會中斷正在執行的工作線程。注意:調用 shutdownNow() 時會中斷正在執行的工作線程。
-
通過實現 Runnable 接口,可以在 run() 方法中調用 ThreadPoolExecutor#runWorker 方法執行任務處理。
提交任務
向線程池中提交任務的方式有兩種:
-
調用 execute() 方法。
-
調用 submit() 方法。
execute 與 submit 的區別:
-
1)execute 是 Executor 接口的方法,submit 是 ExecuteService 接口的方法。
-
2)execute 的入參爲 Runnable,submit 的入參可以爲 Runnable、Callable、Runnable 和一個返回值。
-
3)execute 沒有返回值,submit 有返回值。
-
4)異常處理:execute 會直接拋出異常,submit 會在獲取結果時拋出異常,如果不獲取結果,submit 不拋出異常。
以 execute() 方法爲例,ThreadPoolExecutor 實現了 Executor 接口定義的 execute(Runnable command) 方法,該方法的主要作用是將任務提交到線程池中的執行。
execute(Runnable command) 方法源碼如下:
public void execute(Runnable command) {
// 如果command爲null,則拋出空指針異常
if (command == null)
throw new NullPointerException();
// 獲取ctl變量值,ctl低29位用來表示線程池中的線程數量。
int c = ctl.get();
// 如果線程池中的線程數小於設定的核心線程數,則將任務封裝成Worker線程並調用其start()方法啓動線程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果線程池處於運行狀態並向任務隊列中添加任務成功,則執行如下邏輯(此處線程池中的線程數大於等於核心線程數)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 繼續判斷線程池處於運行狀態,如果線程池不是運行狀態且從線程池中移除任務成功,則執行拒絕策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果線程池中線程數爲0,則創建Worker線程並執行任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果創建Worker線程失敗,則執行拒絕策略
else if (!addWorker(command, false))
reject(command);
}
ThreadPoolExecutore#xecute() 方法執行邏輯,如圖所示:
處理流程:
1)主線程通過 execute() 方法向線程池中提交任務:
-
線程池中的線程數小於核心線程數,繼續向線程池中提交任務時,創建線程執行任務處理。
-
線程池中的線程數大於等於核心線程數:
a) 如果阻塞隊列未滿,繼續向線程池中提交任務時,將任務添加到阻塞隊列中,等待處理。
b) 如果阻塞隊列已滿且線程數小於最大線程數,繼續向線程池中提交任務時,創建線程執行任務處理。
c) 如果阻塞隊列已滿且線程數大於等於最大線程數,繼續向線程池中提交任務時,執行拒絕策略。
執行任務
任務提交到線程池中後,會執行工作線程(Worker)的 ThreadPoolExecutor.Worker#run 方法執行任務處理,而 run() 方法中會調用 ThreadPoolExecutor#runWorker 方法。
ThreadPoolExecutor#runWorker 方法中,通過自旋的方式從 Worker 工作線程或阻塞隊列中獲取任務進行處理:
-
如果 Worker 工作線程中存在任務,則執行 Worker 工作線程中的任務。
-
如果 Worker 工作線程中不存在任務,則通過 getTask() 方法從獲取任務,並執行該任務。
-
如果 Worker 工作線程和阻塞隊列中沒有可執行的任務,則退出並銷燬 Worker 工作線程。
線程池工作流程
線程池工作流程,如圖所示:
處理流程:
1)向線程池中提交任務,判斷線程池中線程數是否小於核心線程數:
-
線程數小於核心線程數,則創建線程執行任務處理。
-
線程數大於等於核心線程數,則判斷線程池中阻塞隊列是否已滿:
a) 阻塞隊列未滿,則將任務添加到阻塞隊列中,等待執行。
b) 阻塞隊列已滿,則判斷線程池中的線程數是否小於最大線程數:
線程數小於最大線程數,則創建線程執行任務處理。
線程數大於等於最大線程數,則執行拒絕策略。
線程池創建方式
線程池的創建方式一般分爲兩種:
-
通過 Executors 類創建線程池。
-
通過 ThreadPoolExecutor 類創建線程池。
通過 Executors 類創建線程池
Executors 類是 JDK 提供的一個創建線程池的工具類,內部通過調用 ThreadPoolExecutor 構造函數來實現,通過該類提供的靜態方法可以快速創建一些常用線程池。
newFixedThreadPool
創建一個固定大小的線程池,可控制併發的線程數,超出的線程會在隊列中等待。
靜態方法:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
其中:
-
核心線程數爲初始化時指定的線程數,核心線程數等於最大線程數。
-
線程存活時間 keepAliveTime 爲 0,表示線程空閒時立刻被回收。
-
阻塞隊列爲 LinkedBlockingQueue(默認值)。
newCachedThreadPool
創建一個可緩存的線程池,該線程池的大小爲 Integer.MAX_VALUE,對於提交的新任務:
-
如果存在空閒線程,則使用空閒線程來執行任務處理。
-
如果不存在空閒線程,則新建一個線程來執行任務處理。
-
如果線程池中的線程數超過處理任務需要的線程數,則空閒線程緩存一段時間(60s)後會被回收。
靜態方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
其中:
-
核心線程數爲 0。
-
最大線程數爲 Integer.MAX_VALUE。
-
線程存活時間 keepAliveTime 爲 60s。
-
阻塞隊列爲 SynchronousQueue(同步隊列)。
newSingleThreadExecutor
創建一個單線程化的線程池,它只有一個工作線程來執行任務,所有任務按照先進先出的順序執行。
靜態方法:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
其中:
-
核心線程數爲 1。
-
最大線程數爲 1。
-
線程存活時間 keepAliveTime 爲 0,表示線程空閒時立刻被回收。
-
阻塞隊列爲 LinkedBlockingQueue。
newScheduledThreadPool
創建一個計劃線程池,支持定時或週期性的執行任務(如:延時任務)。
靜態方法:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
其中:
-
核心線程數爲初始化時指定的線程數。
-
最大線程數爲 Integer.MAX_VALUE。
-
線程存活時間 keepAliveTime 爲 0,表示線程空閒時立刻被回收。
-
阻塞隊列爲 DelayedWorkQueue(延時隊列)。
其他線程池
Executors 工具類除了能快速創建以上的常用線程池外,還可以創建很多其他線程池。如:
-
Executors.newSingleThreadScheduledExecutor:創建一個單線程化的計劃線程池(newScheduledThreadPool 的單線程版本)。
-
Executors.newWorkStealingPool:創建一個搶佔式執行的線程池(任務執行順序不確定),該線程池 JDK1.8 添加。
代碼示例:
ExecutorsExample.java
@Slf4j
public class ExecutorsExample {
/**
* 創建一個固定大小的線程池
*/
private static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
// 在線程池中執行任務
executor.execute(new Task());
}
}
/**
* 任務線程
*/
static class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
log.info("執行線程:{}", Thread.currentThread().getName());
} catch (InterruptedException e) {
log.info("線程中斷異常");
}
}
}
}
不推薦使用 Executors 類創建線程池,主要原因:Executors 類創建線程池默認使用 LinkedBlockingQueue,LinkedBlockingQueue 默認大小爲 Integer.MAX_VALUE(相當於無界隊列),在高負載情況下很容易導致 OOM。因此,強烈建議使用有界隊列創建線程池。
通過 ThreadPoolExecutor 類創建線程池
通過 ThreadPoolExecutor 類手動創建線程池(推薦方式)。
代碼示例:
ThreadPoolExecutorExample.java
@Slf4j
public class ThreadPoolExecutorExample {
/**
* 定義線程池
*/
private static ExecutorService pool ;
public static void main(String[] args) {
// 創建線程池
pool = new ThreadPoolExecutor(10, 20, 0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 在線程池中執行任務
for (int i = 0; i < 10000; i++) {
pool.execute(new ThreadPoolExecutorExample.Task());
}
}
/**
* 任務線程
*/
static class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000);
log.info("執行線程:{}", Thread.currentThread().getName());
} catch (InterruptedException e) {
log.info("線程中斷異常");
}
}
}
}
其他推薦方式:通過 commons-lang3、com.google.guava 等工具包創建線程池,創建示例自行查閱相關資料即可。
線程池大小設置
在併發編程領域,提升性能本質上就是提升硬件的利用率(即:提升 CPU 和 I/O 設備綜合利用率)的問題。因此,線程池大小需要根據 CPU 密集型和 I/O 密集型場景進行設置:
-
對於 CPU 密集型場景,線程池大小一般設置爲:CPU 核數 + 1。
-
對於 I/O 密集型場景,線程池大小一般設置爲:CPU 核數 * [1 +(I/O 耗時 / CPU 耗時)]。
以上公式僅作爲參考值,具體情況需要根據壓測情況進行設置。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/VV2BouUseuotAoy1FIyjqg