Java併發與多線程-詳解線程池
什麼是線程池?
在此,我們參考一下百科的定義:
線程池(英語:thread pool):一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護着多個線程,等待着監督管理者分配可併發執行的任務。這避免了在處理短時間任務時創建與銷燬線程的代價。
線程池的作用?
瞭解線程池作用之前,我們先來看一下,CPU的上下文切換
,有多費時間:
拿一臺主頻2.6G的電腦來說,每秒可以執行
2.6*10^9
個指令,每個指令只需要0.38ns
,爲了方便理解,我們把這個時間單位,換算成人類世界的1秒
。
一次 CPU 上下文切換(系統調用)需要大約1500ns
,也就是1.5us
(這個數字採用的是單核 CPU 線程平均時間),換算成人類時間大約是65分鐘
,嗯,也就是一個小時
。我們也知道上下文切換是很耗時的行爲,畢竟每次浪費一個小時,也很讓人有罪惡感的。上下文切換更恐怖的事情在於,這段時間裏 CPU沒有做任何有用的計算,只是切換了兩個不同進程的寄存器和內存狀態;而且這個過程還破壞了緩存,讓後續的計算更加耗時。
備註:上下文切換(有時也稱做進程切換或任務切換)是指 CPU 從一個進程或線程切換到另一個進程或線程。
如果想讓程序運行的更快,我們需要減少CPU上下文切換的次數。
所以,需要避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態。使用線程池,可以有效避免類似的問題。線程池解決的核心問題就是資源管理問題。對使用到的線程進行統一管理,避免創建不必要的線程。
使用線程池,還可以帶來如下好處:
- 降低資源消耗:通過池化技術重複利用已創建的線程,降低線程創建和銷燬造成的損耗。
- 提高響應速度:任務到達時,無需等待線程創建即可立即執行。
- 提高線程的可管理性:線程是稀缺資源,如果無限制創建,不僅會消耗系統資源,還會因爲線程的不合理分佈導致資源調度失衡,降低系統的穩定性。使用線程池可以進行統一的分配、調優和監控。
- 提供更多更強大的功能:線程池具備可拓展性,允許開發人員向其中增加更多的功能。比如延時定時線程池ScheduledThreadPoolExecutor,就允許任務延期執行或定期執行。
既然線程池有這麼多好處,那我們如何使用呢?
首先,要用好線程池,需要知道線程池的生命週期以及線程池的主要參數配置。
線程池生命週期
ThreadPoolExecutor的運行狀態有5種:
- RUNNING
- SHUTDOWN
- STOP
- TIDYING
- TERMINATED
源碼定義如下:
java.util.concurrent.ThreadPoolExecutor.ctl
文檔註釋摘錄
The runState provides the main lifecycle control, taking on values:
RUNNING: Accept new tasks and process queued tasks
(譯:接受新任務並處理排隊的任務)
SHUTDOWN: Don't accept new tasks, but process queued tasks
(譯:不接受新任務,但處理已排隊的任務)
STOP: Don't accept new tasks, don't process queued tasks,
and interrupt in-progress tasks
(譯:不接受新任務,不處理排隊的任務,並中斷正在進行的任務)
TIDYING: All tasks have terminated, workerCount is zero,
the thread transitioning to state TIDYING
will run the terminated() hook method
(譯:所有任務已終止,workerCount爲零,線程轉換到 TIDYING 狀態,會運行terminated()鉤子方法)
TERMINATED: terminated() has completed
(譯:terminated()方法執行完畢)
圖:線程池5種狀態-流程圖
狀態 | 是否接受新任務 | 排隊任務的處理 | 其他 |
---|---|---|---|
RUNNING | 接受新任務 | 處理排隊的任務 | 正常運行 |
SHUTDOWN | 不接受新任務 | 處理排隊的任務 | 不接受新任務,但處理已排隊的任務 |
STOP | 不接受新任務 | 不處理排隊的任務 | 不接受新任務,不處理排隊的任務,並中斷正在進行的任務 |
TIDYING | 不接受新任務 | - | 所有任務已終止 |
TERMINATED | 不接受新任務 | - | terminated()方法執行完畢 |
詳解線程池參數
圖:線程池參數列表.png
- 第一個參數設置核心線程數。默認情況下核心線程會一直存活。
- 第二個參數設置最大線程數。決定線程池最多可以創建的多少線程。
- 第三個參數和第四個參數用來設置線程空閒時間,和空閒時間的單位,當線程閒置超過空閒時間就會被銷燬。可以通過 allowCoreThreadTimeOut 方法來允許核心線程被回收。
- 第五個參數設置緩衝隊列,上圖中左下方的三個隊列是設置線程池時常使用的緩衝隊列。其中
ArrayBlockingQueue
是一個有界隊列,就是指隊列有最大容量限制。LinkedBlockingQueue
是無界隊列,就是隊列不限制容量。最後一個是SynchronousQueue
,是一個同步隊列,內部沒有緩衝區。 - 第六個參數設置線程池工廠方法,線程工廠用來創建新線程,可以用來對線程的一些屬性進行定製,例如線程的 group、線程名、優先級等。一般使用默認工廠類即可。
- 第七個參數設置線程池滿時的拒絕策略。如上圖右下方所示有四種策略,
Abort
策略在線程池滿後,提交新任務時會拋出RejectedExecutionException
,這個也是默認的拒絕策略。Discard 策略會在提交失敗時對任務直接進行丟棄。CallerRuns 策略會
在提交失敗時,由提交任務的線程直接執行提交的任務。DiscardOldest
策略會丟棄最早提交的任務。
接着,我們需要了解一下線程池的執行流程:
線程池執行流程
圖:線程池任務執行流程.png
- 向線程池提交任務時,會首先判斷線程池中的線程數是否大於設置的核心線程數,如果不大於,就創建一個核心線程來執行任務。
- 如果大於核心線程數,就會判斷緩衝隊列是否滿了,如果沒有滿,則放入隊列,等待線程空閒時執行任務。
- 如果隊列已經滿了,則判斷是否達到了線程池設置的最大線程數,如果沒有達到,就創建新線程來執行任務。
- 如果已經達到了最大線程數,則執行指定的拒絕策略。
這裏需要注意隊列的判斷與最大線程數判斷的順序,不要搞反。
瞭解了理論,接下來,我們通過一個實戰,來實地觀察一下線程池任務的執行流程。
實戰源碼
- 程序目的:觀察
ThreadPoolExecutor
的執行流程
包含如下組成部分: - 一個監聽線程池狀態的類:
MyMonitorThread
,每1秒輸出一次線程池狀態。 - 一個
RejectedExecutionHandlerImpl
,用來執行拒絕策略,會打印那些任務被拒絕了。 - 一個工作線程定義:
WorkerThread
會模擬2秒的任務執行耗時。 - 主程序:
WorkerPoolApplication
用來執行程序
MyMonitorThread.java
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/**
* <pre>
* 監控線程池狀態(每隔N秒,輸出一次線程池的狀態)
* 分別會打印如下信息:
* </pre>
* created at 2019-05-29 14:12
* @author lerry
*/
@Slf4j
public class MyMonitorThread implements Runnable {
/**
* 持有被監控的線程池對象
*/
private ThreadPoolExecutor threadPoolExecutor;
/**
* 每隔多久執行一次
*/
private int delay;
/**
* 如果爲false、則關閉監聽
*/
private boolean isRun = true;
public MyMonitorThread(ThreadPoolExecutor threadPoolExecutor, int delay) {
this.threadPoolExecutor = threadPoolExecutor;
this.delay = delay;
}
public void shutDown() {
this.isRun = false;
}
@Override
public void run() {
while (isRun) {
// 獲取等待隊列
BlockingQueue<Runnable> queue = this.threadPoolExecutor.getQueue();
// 等待隊列轉爲僅存儲 指令名稱 的List
List<String> queueList = queue.stream().map(r -> {
if (r instanceof WorkerThread) {
return ((WorkerThread) r).getCommand();
}
else {
return "";
}
}).collect(Collectors.toList());
// 日誌記錄線程池狀態
/*
* poolSize:池中的當前線程數
* corePoolSize: 核心線程數
* Active:當前主動執行任務的近似線程數量
* Completed:已完成執行的任務的總數(由於任務和線程的狀態在計算過程中可能會動態變化,因此返回的值僅是一個近似值,而在連續的調用中不會降低。)
* TaskCount:計劃執行的任務數
* queue:緩衝隊列
* isShutdown:如果此執行程序已關閉,則返回true
* isTerminated:觀察線程池是否終結
*/
log.info("poolSize/corePoolSize [{}/{}] Active: {}, Completed: {}, Task: {}, queue:{},isShutdown: {}, isTerminated: {}",
this.threadPoolExecutor.getPoolSize(),
this.threadPoolExecutor.getCorePoolSize(),
this.threadPoolExecutor.getActiveCount(),
this.threadPoolExecutor.getCompletedTaskCount(),
this.threadPoolExecutor.getTaskCount(),
queueList,
this.threadPoolExecutor.isShutdown(),
this.threadPoolExecutor.isTerminated());
// 間隔N秒輸出一次線程池狀態
try {
Thread.sleep(delay * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
RejectedExecutionHandlerImpl.java
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.extern.slf4j.Slf4j;
/**
* 還可以創建自己的 RejectedExecutionHandler 實現來處理沒有放在工作隊列裏的任務。
* rejected:拒絕的
* created at 2019-05-29 14:09
* @author lerry
*/
@Slf4j
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
WorkerThread workerThread = null;
if (r instanceof WorkerThread) {
workerThread = (WorkerThread) r;
}
log.info("[{}] is rejected(被拒絕/駁回)", workerThread.getCommand());
}
}
WorkerThread.java
/**
* 工作線程
* created at 2019-05-29 11:25
* @author lerry
*/
@Slf4j
public class WorkerThread implements Runnable {
/**
* 執行的任務編號
*/
private String command;
/**
* 模擬工作線程的執行耗時(單位:秒)
*/
private int executeDuration;
public WorkerThread(String command) {
this.command = command;
}
public WorkerThread(String command, int executeDuration) {
this.command = command;
this.executeDuration = executeDuration;
}
@Override
public void run() {
log.info("{} Start. Command = {}", Thread.currentThread().getName(), command);
processCommand();
log.info("{} End. Command = {}", Thread.currentThread().getName(), command);
}
/**
* 模擬任務執行耗時
*/
private void processCommand() {
try {
Thread.sleep(executeDuration * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public String getCommand() {
return command;
}
}
WorkerPoolApplication.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* <pre>
* 程序目的:觀察ThreadPoolExecutor的執行流程
* 包含:一個監聽線程池狀態的類:MyMonitorThread,每1秒輸出一次線程池狀態。
* 一個RejectedExecutionHandlerImpl,用來執行拒絕策略,會打印那些任務被拒絕了。
* 一個工作線程定義:WorkerThread 會模擬2秒的執行時間。
* 主程序:WorkerPoolApplication 用來執行程序
* 請注意:在初始化 ThreadPoolExecutor 時,核心線程數大小設爲2、最大線程數設爲5、緩衝隊列大小設爲2。
* 所以,一共提交10個任務的情況下,前2個任務,線程池會創建核心線程、並執行該任務;
* 第3——4個任務,會進入等待隊列,隊列滿了之後,
* 第5——7個任務,會繼續創建線程、執行該任務;
* 最後的第8、9、10個任務,會執行拒絕策略,交由RejectedExecutionHandlerImpl 處理,被rejected。
* </pre>
* created at 2019-05-29 14:16
* @author lerry
*/
@Slf4j
public class WorkerPoolApplication {
public static void main(String[] args) throws InterruptedException {
RejectedExecutionHandlerImpl rejectedExecutionHandler = new RejectedExecutionHandlerImpl();
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 線程池
/*
* corePoolSize:核心線程數 默認情況下核心線程會一直存活
* maximumPoolSize:最大線程數 決定線程池最多可以創建的多少線程
* keepAliveTime、unit:線程空閒時間,和空閒時間的單位 當線程閒置超過空閒時間就會被銷燬
* workQueue:緩衝隊列
* threadFactory:設置線程池工廠方法,線程工廠用來創建新線程,可以用來對線程的一些屬性進行定製,例如線程的 group、線程名、優先級等
* RejectedExecutionHandler: 設置線程池滿時的拒絕策略
*/
ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2),
threadFactory,
rejectedExecutionHandler
);
// 啓動監控線程
MyMonitorThread monitor = new MyMonitorThread(executorPool, 1);
new Thread(monitor).start();
for (int i = 1; i <= 10; i++) {
executorPool.execute(new WorkerThread("cmd" + i, 2));
}
log.info("關閉線程池");
executorPool.shutdown();
for (; ; ) {
// 線程池終止後,關閉監控線程
if (executorPool.isTerminated()) {
// 等待監視器打印線程池 isTerminated: true 的狀態
Thread.sleep(1_000);
monitor.shutDown();
break;
}
}// end for
}
}
執行結果
2020-07-05 11:00:10.287 [pool-1-thread-3] INFO WorkerThread - pool-1-thread-3 Start. Command = cmd5
2020-07-05 11:00:10.287 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 Start. Command = cmd6
2020-07-05 11:00:10.287 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 Start. Command = cmd7
2020-07-05 11:00:10.287 [pool-1-thread-2] INFO WorkerThread - pool-1-thread-2 Start. Command = cmd2
2020-07-05 11:00:10.287 [pool-1-thread-1] INFO WorkerThread - pool-1-thread-1 Start. Command = cmd1
2020-07-05 11:00:10.287 [main ] INFO RejectedExecutionHandlerImpl - [cmd8] is rejected(被拒絕/駁回)
2020-07-05 11:00:10.294 [main ] INFO RejectedExecutionHandlerImpl - [cmd9] is rejected(被拒絕/駁回)
2020-07-05 11:00:10.294 [main ] INFO RejectedExecutionHandlerImpl - [cmd10] is rejected(被拒絕/駁回)
2020-07-05 11:00:10.294 [main ] INFO WorkerPoolApplication - 關閉線程池
2020-07-05 11:00:10.359 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
2020-07-05 11:00:11.364 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
2020-07-05 11:00:12.294 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 End. Command = cmd7
2020-07-05 11:00:12.294 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 End. Command = cmd6
2020-07-05 11:00:12.294 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 Start. Command = cmd3
2020-07-05 11:00:12.294 [pool-1-thread-2] INFO WorkerThread - pool-1-thread-2 End. Command = cmd2
2020-07-05 11:00:12.294 [pool-1-thread-3] INFO WorkerThread - pool-1-thread-3 End. Command = cmd5
2020-07-05 11:00:12.294 [pool-1-thread-1] INFO WorkerThread - pool-1-thread-1 End. Command = cmd1
2020-07-05 11:00:12.294 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 Start. Command = cmd4
2020-07-05 11:00:12.366 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
2020-07-05 11:00:13.370 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
2020-07-05 11:00:14.297 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 End. Command = cmd4
2020-07-05 11:00:14.297 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 End. Command = cmd3
2020-07-05 11:00:14.374 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [0/2] Active: 0, Completed: 7, Task: 7, queue:[],isShutdown: true, isTerminated: true
執行結果解讀
首先,工作線程5、6、7和2、1被創建並啓動,一共10個任務,但是線程池最大線程數設置的是5
,等待隊列大小設置的是2
,剩下的三個線程(8、9、10),被執行拒絕策略。
這時,我們手動調用shutdown()
,嘗試關閉線程池。因爲還有線程未執行完,等待隊列中也有任務,所以線程池會等待事情全部處理好後,再關閉。
這時,通過監控日誌,可以發現:
poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
當前池中線程數爲5、計劃執行的線程數爲7,3、4號工作線程在隊列中。
接着,7和6執行完畢,隊列中的3號任務開始執行,2、5、1也相繼執行完畢,隊列中的4號任務開始執行。這時再次觀察線程池狀態:
poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
可以看到,當前池中線程數爲2、已完成執行的任務的總數爲5,計劃執行的線程數爲7,隊列爲空。
繼續,4號和3號線程執行完畢,最後查看線程池狀態:
poolSize/corePoolSize [0/2] Active: 0, Completed: 7, Task: 7, queue:[],isShutdown: true, isTerminated: true
可以看到,當前池中線程數爲0、已完成執行的任務的總數爲7,計劃執行的線程數爲7,隊列爲空。線程池關閉。
我們發現、超過線程池核心線程數的、小於最大線程數的這部分線程,優先於等待隊列中的任務執行。
目錄結構
圖:本文目錄結構
參考資料
線程池_百度百科
讓 CPU 告訴你硬盤和網絡到底有多慢 | Cizixs Write Here
多線程上下文切換 - 五月的倉頡 - 博客園
Java線程池實現原理及其在美團業務中的實踐 - 美團技術團隊
環境說明
- java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
- OS:
macOS High Sierra 10.13.4
- 日誌:
logback