手把手教你寫出一個Java線程池
線程池架構
- 一個線程池,應包含阻塞隊列,用來存放任務
- 包含存放線程的集合,其中的線程用來直接執行任務,或拉取緩存隊列中的任務
注意,下方代碼基本基於本架構圖編寫
自定義拒絕策略
使用函數式接口,將拒絕權限下方,由調用者決定添加任務失敗時線程池的回絕方式
- 死等
- 帶超時等待
- 讓調用者放棄任務執行
- 讓調用者拋出異常
- 讓調用者自己執行任務
//步驟一:自定義拒絕策略接口
@FunctionalInterface //函數式接口
interface RejectPolicy<T>{
void reject(BlockingQueue<T> queue, T task) ;
}
自定義阻塞隊列
阻塞隊列用來暫存任務,超過線程池核心線程數的部分線程,將存放在阻塞隊列中,如果阻塞隊列也滿了,將拒絕繼續向阻塞隊列中添加任務
//自定義阻塞隊列
class BlockingQueue<T>{
//任務隊列
private Deque<T> queue = new ArrayDeque<>() ; //雙向隊列:對頭、隊尾都可操作
//鎖,隊列的頭部元素獲取與尾部元素添加都需要在鎖中完成
private ReentrantLock lock = new ReentrantLock();
//生產者條件變量(生產者的等待隊列),在任務隊列滿的時候,添加任務的線程進入等待
private Condition fullWaitSet = lock.newCondition() ;
//消費者條件變量,在任務任務隊列空的時候,線程不再集合拉取任務
private Condition emptyWaitSet = lock.newCondition();
// 隊列容量
private int capcity;
//構造器,只需要調用者給出容量即可
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
//poll->返回任務隊列頭部的元素給消費者(線程集合)
// 超時機制版的阻塞獲取,一定時間內無任務則不再進行獲取
public T poll(long timeout, TimeUnit unit) {
lock.lock(); //獲取操作需要同步
try {
//將 timeout 統一轉換爲 納秒
long nanos = unit.toNanos(timeout);
//任務隊列中已經空了(無任務)
while (queue.isEmpty()) {
try {
//等待時間超時,返回空值
if (nanos <= 0) {
return null;
}
// 隊列空的等待操作
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//任務隊列不空,返回隊列頭部元素
T t = queue.removeFirst();
//通知向隊列添加任務的線程,可以繼續添加任務
fullWaitSet.signal();
return t;
}
finally {
lock.unlock(); //解除鎖
}
}
//普通版阻塞隊列獲取任務
public T take(){
lock.unlock();
try {
while (queue.isEmpty()){
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t ;
}finally {
lock.unlock();
}
}
//阻塞隊列,任務的添加
public void put(T task){
lock.lock(); //添加需要上鎖
try {
//隊列滿
while (queue.size()==capcity) {
try {
//進入等待,等待可以放入任務的允許通知
System.out.println("等待加入任務隊列"+task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("加入任務隊列"+task);
queue.addLast(task); //向隊列尾部添加任務
emptyWaitSet.signal();//告訴消費任務的線程,可以消費了
}finally {
lock.unlock(); //解鎖
}
}
//超時機制版阻塞隊列,任務的添加
public boolean offer(T task,long timeout,TimeUnit unit){
lock.lock();
try {
long nanos = unit.toNanos(timeout);
//隊列滿
while (queue.size()==capcity) {
try {
if(nanos <= 0) {
return false;
}
//等待上面的通知就完事
System.out.println("等待加入任務隊列"+task);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("加入任務隊列"+task);
queue.addLast(task);
emptyWaitSet.signal();//廣播 隊列不爲空了,喚醒等待隊列不爲空的線程(在上面)
return true;
}finally {
lock.unlock();
}
}
//獲取任務隊列大小
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
//帶拒絕策略的put,即如果任務隊列無法添加元素,使用何種策略進行拒絕添加
public void tryPut(RejectPolicy<T> rejectPolicy,T task){
lock.lock();
try {
//判斷隊列是否滿,如滿,執行拒絕策略 --> 策略模式
if (queue.size() == capcity) rejectPolicy.reject(this,task);
else {
System.out.println("加入任務隊列"+task);
queue.addLast(task);
emptyWaitSet.signal();//廣播 隊列不爲空了,喚醒等待隊列不爲空的線程(在上面)
}
}finally {
lock.unlock();
}
}
}
小結:
- 本代碼實現了阻塞隊列頭元素獲取及尾元素添加
- 將阻塞隊列中的各個任務存放於雙端隊列
- 構造器將初始化該阻塞隊列的核心容量
- 添加與獲取都提供了超時回空策略,避免長時間無添加與獲取造成的性能浪費
自定義線程池
//自定義線程池
class ThreadPool{
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.rejectPolicy = rejectPolicy;
this.taskQueue = new BlockingQueue<>(queueCapcity) ;
}
//單個線程的封裝,使用Worker對線程進行了擴展
class Worker extends Thread{
private Runnable task ;
public Worker(Runnable task) {
this.task = task;
}
public void run(){
// 執行任務
// 1) 當 task 不爲空,執行任務
// 2) 當 task 執行完畢,再接着從任務隊列獲取任務並執行
while (task!=null||(task=taskQueue.poll(timeout,timeUnit))!=null){
try {
System.out.println("正在執行"+task);
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
task = null ;
}
}
synchronized (workers){
System.out.println("worker被移除"+this);
workers.remove(this);
}
}
}
//任務隊列
private BlockingQueue<Runnable> taskQueue ;
//線程集合,集合中使用Worker對線程進行了擴展
private HashSet<Worker> workers = new HashSet<>();
//核心線程數
private int coreSize ;
//獲取任務時的超時時間
private long timeout ;
//超時時間的單位規範
private TimeUnit timeUnit ;
//拒絕策略,由主線程自己決定,主線程取實現這個RejectPolicy,並傳入本線程池即可
private RejectPolicy<Runnable> rejectPolicy ;
//執行任務
public void execute(Runnable task){
// 當任務數沒有超過 coreSize 時,直接交給 worker 對象執行,注意,Work執行完畢後不能立即消失,要被循環利用
// 如果任務數超過 coreSize 時,加入任務隊列暫存
synchronized (workers){
//如果線程集合中的線程數量少於核心線程數,創建Work對象來執行任務
if (workers.size()<coreSize){
Worker worker = new Worker(task);
System.out.println("新增worker"+worker+"task"+task);
workers.add(worker) ;
worker.start();
}else {
// 各種拒絕策略
// 1) 死等
// 2) 帶超時等待
// 3) 讓調用者放棄任務執行
// 4) 讓調用者拋出異常
// 5) 讓調用者自己執行任務
//加入任務隊列,等待被執行
taskQueue.tryPut(rejectPolicy,task);
}
}
}
}
小結:
這就是融合了阻塞隊列與拒絕策略的完整線程池了,包括
- 阻塞隊列
- 線程集合
- 執行方法
- 創建線程來執行任務
- 如果線程集合中的線程數少於線程池容量,則創建線程直接執行任務
- 如果線程集合中的線程數大於線程池容量,則將任務放入阻塞隊列,由線程執行完任務後拉去阻塞隊列中的任務來執行
- 創建線程來執行任務
測試方法
public class MyThreadPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1,
1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{
// 1. 死等
// queue.put(task);
// 2) 帶超時等待
// queue.offer(task, 1500, TimeUnit.MILLISECONDS);
// 3) 讓調用者放棄任務執行
// log.debug("放棄{}", task);
// 4) 讓調用者拋出異常
// throw new RuntimeException("任務執行失敗 " + task);
// 5) 讓調用者自己執行任務
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(j); //執行打印j
});
}
}
}
方便演示拒絕策略是否生效,我們指定:
- 線程池中只有1個線程,超時等待時間爲1s,阻塞隊列容量爲1個
拒絕策略我們選擇:
- 如果線程池拒絕接收任務,則調用線程池的線程自己把任務執行掉
結果: