概述
做後端的應該都知道線程池,即使你沒親自使用過,那也一定聽過或者瞭解過。有時候也會去深入理解,結果往往是當時覺得自己理解了,過一段時間就忘了。因爲在日常的開發中,我們都不需要用到線程池,很多都是使用的工具和框架寫好,我們直接調接口就完事了。
很多東西沒有親自實踐和深入的思考過的,單單看文章和書籍是不可能真正的理解的。以前我看了好多次線程池相關的文章,然後過個半年忘得差不多,又重新看,結果還是沒有真正的理解。所以,我就打算動手實踐一番,既然平時開發的時候用不到,那我就自己做一個項目來用上。
說做就做,我就選了一個Redis分佈式鎖作爲練手項目,裏面有一個定時續期的功能,就是使用線程池定時的運行提交的任務,將key續期,詳細的不說了,想了解可以看這個 Redis分佈式鎖的實現-Redisson。
在實現Redis續期功能的時候,一邊看別人定時任務怎麼實現的,一邊看線程池的源碼。這時候我彷彿打開了新世界的大門,徹底理解了線程池運行邏輯,也瞭解了一些線程池設計的藝術。
接下來我想以一個設計者的角度,帶領大家從零去設計和實現一個線程池,一步一個腳印,徹底的理解線程池的實現以及一些設計的藝術。
線程池出現的目的和意義
我們要明白,任何技術都是爲了解決問題而出現的。那麼線程池的出現解決了什麼問題呢,答案是:解決了線程資源複用的問題
。
如果沒有線程池,我們處理一個任務就要新開一個線程去執行,當任務完成時,該線程就停止了。如果說這個任務是重複的,總不能來一個就新建一個線程吧,多浪費,而且線程是稀缺資源,重複的創建銷燬很耗時間。
有了線程池,我們就可以建立幾個固定線程。有任務來了喚醒閒置的線程去處理,任務處理完成後繼續處理後續的任務,如果暫時沒有任務,可以將線程休眠,有新任務時再喚醒。這樣一來就可以更高效的利用線程資源,提高系統併發效率。
任務:抽象的工作單元
在程序中,都是圍繞着任務執行
來構造的,任務通常是一些抽象的工作單元。比如可以把一個 http請求 當做是一個任務,把一次與數據庫的交互當做任務等等。在線程池中,我們把要處理的東西抽象成一個任務單元
, 這樣可以簡化線程池的結構,以此更好的構建線程池的模型。
線程:抽象的工作者
在線程池中,我們可以把每一個線程當做是一個worker
,即"工人"的意思。它會不斷的嘗試獲得任務來執行,如果沒有任務,則休眠或者做其他處理。
線程池的功能設計
那麼,線程池通常要具備和提供什麼功能呢,這裏把核心的功能需求給羅列一下:
線程池的開啓和關閉
線程池作爲一個工具,需要有自己的生命週期,可以抽象成三個:
- 開啓狀態
- 運行狀態
- 結束狀態
其中結束狀態下線程池的處理和考慮的東西要多一些,執行完線程池的關閉接口後:
- 正在運行的任務怎麼處理?
- 在任務隊列的任務要怎麼處理?
- 此時線程池是否還能繼續添加任務?
這些東西都是要考慮的並且去處理的。在Java的ExecutorService
提供了兩個關閉接口
shutdown
: 有序的關閉,已提交的任務會被逐一處理,但不會接受任何新任務shutdownNow
: 嘗試停止所有正在執行的任務,放棄在隊列中等待的任務,並返回正在等待執行的任務列表
線程的構建和管理
線程池裏線程該怎麼構建,構建完後怎麼管理,是固定的幾個還是動態的構建。這裏給出幾個模式:
固定的線程數量
:在線程池啓動時就構建固定數量的線程池,且不會關閉任何線程動態構建線程
:啓動時不新建任何線程,當有任務來臨時纔會去創建線程。如果任務比較少,則不會繼續新建線程,如果任務比較多,則繼續構建線程數,直到數量達到最大值。有閒置期限的線程
:線程在構建時會有一個閒置的期限,當閒置的時間超過期限時,該線程就會進行回收處理。這個在數據庫連接池比較常用到單個線程
:只有一個線程,任務按提交的時間順序執行。
任務管理
在線程池中,會建立一個任務隊列,當沒有空閒線程時,新來的任務會放到隊列中,等待線程執行。
線程池要提供任務執行的接口。
另外,很多任務都會將處理結果作爲返回值的,這時任務要有一個完成後的處理機制,在任務完成時做某些操作。(這裏就要涉及到FutureTask
相關概念了)
任務相關的功能如下:
- 任務的提交
- 任務處理結果
- 任務的取消和中斷
線程池模型的構建
梳理了線程池的一些基本功能和要考慮的點,那麼線程池的執行過程是怎樣,要怎麼設計呢。廢話不說,直接上圖:
當有新任務時查看是否有空閒線程,如果有,直接處理,如果沒有則放到任務隊列中,等待線程處理。
其實梳理一下線程池,可以發現它的邏輯並不複雜,複雜的是各種情況的處理,比如線程怎麼管理,任務取消怎麼處理,線程中斷如何處理等等,還有各種併發操作的處理。
使用代碼實現簡易的線程池
接下來實現一個固定數量的線程池,當有任務提交時
線程池要提供的接口
- 任務的提交
線程池內部要實現的功能
- 任務隊列的實現
- 線程管理
咱們暫時將線程池的核心功能簡單的實現,瞭解線程池的執行邏輯,其他的之後慢慢添加。
創建任務單元
首先將任務單元給實現了,直接實現Runnable
接口即可。
當然,可以不實現 Runnable
接口,隨便寫一個類,給一個執行接口,但是呢這樣線程池就不夠通用了,還是直接實現Runnable
接口,往後任意實現該接口的任務都可以交給線程池執行。
static class Task implements Runnable{
private int tag;
public Task(int tag){
this.tag = tag;
}
@Override
public void run() {
System.out.printf("任務 %d 開始執行 \n",tag);
System.out.printf("任務 %d 執行中 \n",tag);
System.out.printf("任務 %d 執行結束\n",tag);
}
}
線程池的實現
詳細的說明在註釋中,看註釋就可以了
package steap1;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadPoolExecutor {
//工作線程數組
private Worker[] workers;
//任務阻塞隊列,是線程安全的,裏面每個操作都會加鎖處理
private BlockingQueue<Task> queue;
// 當前工作線程的數量
private int workerSize = 0;
//線程池最大的工作線程數量
private int poolSize;
public ThreadPoolExecutor(int poolSize, BlockingQueue<Task> queue) {
this.poolSize = poolSize;
this.workers = new Worker[poolSize];
this.queue = queue;
}
public void execute(Task task) {
//如果線程池的線程數量小於最大值,則添加線程
//否則將任務放入隊列中
if (workerSize < poolSize) {
addWorker(task);
} else {
this.queue.add(task);
}
}
//添加worker工作線程,並立即執行
private synchronized void addWorker(Task task) {
//這裏做個雙重判定,判定線程數量是否小於最大值
if (workerSize >= poolSize) {
this.queue.add(task);
return;
}
//構建worker,並啓動線程
workers[workerSize] = new Worker(task);
workers[workerSize].t.start();
workerSize++;
}
//實際運行的代碼
void runWorker(Worker worker){
Task task =(Task) worker.task;
try {
while (true){
//線程在這個循環中不斷的獲取任務來執行
// queue.task() 方法是一個線程安全的阻塞方法
//如果隊列沒有任務,那麼所有工作線程都會在這裏阻塞,等待獲取可用的任務
if(task == null){
task = this.queue.take();
}
task.run();
task = null;
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
//工作線程包裝類
private class Worker implements Runnable {
private Runnable task;
final Thread t;
public Worker(Runnable task) {
this.task = task;
this.t = new Thread(this);
}
@Override
public void run() {
runWorker(this);
}
}
//任務類
static class Task implements Runnable {
private int tag;
public Task(int tag) {
this.tag = tag;
}
@Override
public void run() {
System.out.printf("任務 %d 開始執行 \n", tag);
System.out.printf("任務 %d 執行中 \n", tag);
System.out.printf("任務 %d 執行結束\n", tag);
}
}
}
簡單的使用
public static void main(String[] args){
ThreadPoolExecutor executor = new ThreadPoolExecutor(8,new LinkedBlockingQueue<>());
for(int i=0;i<1000;i++){
executor.execute(new ThreadPoolExecutor.Task(i));
}
}
執行結果
任務 923 開始執行
任務 923 執行中
任務 923 執行結束
任務 912 開始執行
任務 912 執行中
任務 912 執行結束
總結
至此,一個簡單的線程池就編寫完畢,線程池主要的功能都實現了,整個執行過程也進行了詳細的描述。
其實這裏還有很多東西沒寫上,線程的生命週期管理,任務的取消和線程的中斷等等,這些東西在下一篇章完善吧。
結尾附上項目的源代碼,本章的內容在step1
中。