理解线程池原理 - - 手写一个线程池
实现思路
用户创建出线程池对象,自定义线程池的核心线程数、阻塞队列大小、拒绝策略。
- 线程池创建核心线程执行用户提交的任务
- 当线程池中的核心线程都处于非空闲状态时,用户提交的任务则会进入阻塞队列中等待空闲线程执行
- 当阻塞队列已满时,后面提交的任务都会采取相应的拒绝策略
本文只是简单的模拟线程池的实现,舍去了原本线程池中最大线程数、keepAliveTime等实现。
具体实现
阻塞队列
阻塞队列的实现其实就是根据生产者消费者模型来实现的,这里使用了双向链表Deque实现,遵循队列的FIFO原则,下面我们模拟了一个线程安全的阻塞队列。
public class BlockQueue<T> {
private static final Log LOG = LogFactory.get();
/**
* 双向链表
*/
private Deque<T> deque = new ArrayDeque<>();
/**
* 队列长度
*/
private int queueSize;
/**
* 锁
*/
private ReentrantLock lock = new ReentrantLock();
/**
* 生产者条件变量(等待队列)
*/
private Condition fullWaitSet = lock.newCondition();
/**
* 消费者条件变量(等待队列)
*/
private Condition emptyWaitSet = lock.newCondition();
public BlockQueue(int queueSize) {
this.queueSize = queueSize;
}
/**
* 存放一个对象进队列中,阻塞式
* @param t
*/
public void put(T t) {
lock.lock();
try {
// 当队列为满时
while (deque.size() >= queueSize) {
try {
// 进入等待队列 阻塞等待
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// push
LOG.debug("添加阻塞队列: {}", t);
deque.add(t);
// 唤醒poll等待的线程
emptyWaitSet.signalAll();
} finally {
lock.unlock();
}
}
/**
* 尝试存放一个对象进队列中,成功返回true,否则返回false
* @param t
* @return boolean
*/
public boolean tryPut(T t) {
lock.lock();
try {
// 当队列为满时
if (deque.size() >= queueSize) {
return false;
}
// push
LOG.debug("添加阻塞队列: {}", t);
deque.add(t);
// 唤醒poll等待的线程
emptyWaitSet.signalAll();
return true;
} finally {
lock.unlock();
}
}
/**
* 从队列中取出一个对象,超时退出
* @param timeout 超时时间
* @param timeUnit 时间单位
* @return
*/
public T poll(long timeout, TimeUnit timeUnit) {
// 转换时间为纳秒
long waitTime = timeUnit.toNanos(timeout);
lock.lock();
try {
// 队列为空时
while (deque.isEmpty()) {
if (waitTime <= 0) {
return null;
}
// 进入等待队列
try {
// 改方法返回 剩余等待的时间
// 如果不这样操作 假设用户传递超时时间为2秒,该方法在等待1秒后被唤醒,那么可能下一轮在进入while循环,就还要在等待2秒钟
waitTime= emptyWaitSet.awaitNanos(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// poll元素
T t = deque.removeFirst();
// 唤醒push等待的线程
fullWaitSet.signalAll();
return t;
} finally {
lock.unlock();
}
}
/**
* 从队列中取出一个对象
* @return obj
*/
public T take() {
lock.lock();
try {
// 队列为空时
while (deque.isEmpty()) {
try {
// 进入等待队列 阻塞等待
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// poll元素
T t = deque.removeFirst();
// 唤醒push等待的线程
fullWaitSet.signalAll();
return t;
} finally {
lock.unlock();
}
}
}
-
上述的put、take方法都为阻塞式方法,即当队列为满时,调用put方法会一直阻塞,知道队列中可用;当队列为空时,调用take方法会一直阻塞,知道队列不为空。
-
poll方法需要传入时间参数,即在指定时间内获取不到队列中元素,则 return null,不会一直阻塞等待。
注意,这里awaitNanos方法返回的是剩余等待的时间,例如传递时间参数为3秒,该方法等待2秒就被唤醒了,则返回值为1秒。
-
tryPut方法,即尝试存放一个对象进阻塞队列中,该方法不会阻塞,立即返回boolean值结果。
线程池
public class ThreadPool {
private static final Log LOG = LogFactory.get();
/**
* 阻塞队列
*/
private BlockQueue<Runnable> taskQueue;
/**
* 线程集合
*/
private HashSet<Worker> workers = new HashSet<>();
/**
* 核心线程数
*/
private int corePoolSize;
/**
* 获取任务时的超时时间
*/
private long timeout;
/**
* 时间单位
*/
private TimeUnit timeUnit;
/**
* 拒绝策略
*/
private final RejectStrategy<Runnable> rejectStrategy;
public ThreadPool(int corePoolSize, int queueCapacity, long timeout, TimeUnit timeUnit, RejectStrategy<Runnable> rejectStrategy) {
this.corePoolSize = corePoolSize;
this.taskQueue = new BlockQueue<>(queueCapacity);
this.timeout = timeout;
this.timeUnit = timeUnit;
this.rejectStrategy = rejectStrategy;
}
public void execute(Runnable task) {
synchronized (workers) {
if(workers.size() < corePoolSize) {
Worker worker = new Worker(task);
LOG.debug("创建worker线程: {}", worker.getName());
workers.add(worker);
worker.start();
} else {
// 尝试加入阻塞队列
boolean flag = taskQueue.tryPut(task);
if (!flag) {
// 采取拒绝策略
this.rejectStrategy.reject(taskQueue, task);
}
}
}
}
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
while (task != null || (task = taskQueue.take()) != null) {
// while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
LOG.debug("worker线程: {}, 正在运行task: {}", Thread.currentThread().getName(), task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
LOG.debug("删除worker: {}", this.getName());
workers.remove(this);
}
}
}
}
-
这里重点就是execute方法,接收用户需要执行的任务,如果当前worker线程数(核心线程数)未超过corePoolSize,则创建出Worker对象,执行用户提交的任务。如果当前worker线程数已经达到了corePoolSize大小,则调用tryPut方法尝试将该任务加入到阻塞队列中,如果队列已满,则调用reject方法,采取相应的拒绝策略。
-
这里的Worker对象其实就是作为线程池中的一个线程,它继承了Thread对象,重写了run方法。在其run方法中,正常情况下,第一轮while循环时,task即为用户需要执行的任务,直接调用了task的run方法,即表示使用当前worker线程去执行用户需要执行的任务,执行完毕后,置task为null,接下来的每一次循环,都会去阻塞队列中获取等待执行任务,继续使用当前的worker线程执行。
这里强调一下start和run方法的区别:
- start:调用该方法,jvm会重新创建出一个新的线程执行run方法中的代码。
- run:相当于普通方法的调用,使用当前调用该方法的线程去执行run方法。
- 这里如果使用take阻塞式方法获取任务,当队列为空时,该worker线程会一直处于waiting等待状态,永远不会结束。
- 我们也可以使用poll方法从队列中获取任务,指定超时时间,如果在该时间内依旧获取不到队列中的元素时,那就不一直等了,直接跳出while循环,从线程集合中删除该worker线程。
-
关于一个问题:线程池中的核心线程会不会被回收?
在实际的线程池中,核心线程的回收由
allowCoreThreadTimeOut
参数来控制,默认为false,即不会被收。当设置为true时,不管是核心线程还是非核心线程,只要其空闲时间达到指定的KeepAliveTime时,都会被回收。
拒绝策略
在实际的线程池中,拒绝策略被定义为多种,比如抛出异常、死等、记录日志、让调用者自己执行等等。因此,在实现过程中,我们可以通过实现接口的方式,自定义多种不同的拒绝策略。
@FunctionalInterface // 函数式接口
interface RejectStrategy<T> {
/**
* 拒绝策略具体实现
* @param queue 阻塞队列
* @param task 任务
*/
void reject(BlockQueue<T> queue, T task);
}
测试
创建一个线程池,参数如下:
- 核心线程数为1
- 阻塞队列大小为1
- 超时获取元素时间为2秒
- 拒绝策略:记录日志
假设有3个任务交给该线程池执行,每个任务执行耗时3秒钟。
public static void main(String[] args) {
final Log log = LogFactory.get();
ThreadPool threadPool = new ThreadPool(1, 1, 2, TimeUnit.SECONDS, (queue, task) -> {
// 拒绝策略:
// 1. 记录日志
log.debug("task: {}, 未能加入阻塞队列", task);
// 2. 抛出异常
// throw new RuntimeException("队列已满, 任务执行失败: " + task);
// 3. 让调用者自己执行任务
// task.run();
// .......
});
for (int i = 0; i < 3; i++) {
threadPool.execute(() -> {
try {
// 假设每个任务执行时间为3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
使用take方法阻塞式获取,output:
[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.ThreadPool: 创建worker线程: Thread-0
[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.BlockQueue: 添加阻塞队列: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:00:27] [DEBUG] com.ao.demo8.Demo01: task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87, 未能加入阻塞队列
[2023-02-22 14:00:30] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
即在处理完任务之后,worker线程会一直处于等待状态,程序一直运行。
使用poll方法指定获取时间,output:
[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.ThreadPool: 创建worker线程: Thread-0
[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.BlockQueue: 添加阻塞队列: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:08:00] [DEBUG] com.ao.demo8.Demo01: task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87, 未能加入阻塞队列
[2023-02-22 14:08:03] [DEBUG] com.ao.demo8.ThreadPool: worker线程: Thread-0, 正在运行task: com.ao.demo8.Demo01$$Lambda$14/134310351@15bfd87
[2023-02-22 14:08:08] [DEBUG] com.ao.demo8.ThreadPool: 删除worker: Thread-0
线程在处理完包括队列中的任务时,2秒过后,发现未能从队列中获取到新的任务,则不会继续等待,直接释放该worker线程,程序停止运行。
具体流程如下: