线程池基本组件
首先我们思考线程池需要哪些东西
从平时使用jdk提供的线程池时可以看出,我们使用一个线程池时,一般需要一个阻塞队列和一个拒绝策略,接下来我们一一实现。
实现一个拒绝策略接口
实现一个拒绝策略接口,可以看到拒绝策略是一个函数式接口,有一个无返回值的reject方法,用于在我们创建线程池时自定义拒绝策略
@FunctionalInterface
interface MyRejectPolicy<T> {
void reject(MyBlockingQueue<T> queue, T task);
}
实现一个阻塞队列
阻塞队列是一个经典的生产者消费者模型,应该包含以下基本变量
可以看到有一个任务队列用以存储任务,和一个ReentrantLock锁变量用以保证线程安全,并定义了队满等待条件变量fullWaitSet和队空等待条件变量emptyWaitSet 以及队伍容量capcity
// 1. 任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2. 锁
private ReentrantLock lock = new ReentrantLock();
// 3. 队满等待条件变量
private Condition fullWaitSet = lock.newCondition();
// 4. 队空等待条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5.容量
private int capcity;
//构造方法
public MyBlockingQueue(int capcity) {
this.capcity = capcity;
}
那么阻塞队列需要实现哪些方法呢,一般来说至少要实现从队列取一个任务的方法和往队伍中插入一个任务的方法
取任务方法poll()
取任务方法在队为空时,在队空条件变量上等待,否则取走一个队头元素并唤醒在队满条件变量上的等待的线程
public T poll() {
lock.lock();
try {
//队为空,在队空条件上等待
while(queue.isEmpty()){
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.getFirst();
//拿走了一个元素,唤醒在队满条件上等待的线程
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
插入任务方法offer(T t)
插入任务方法在队满时,在队满条件变量上等待,否则往队列中插入一个元素并唤醒在队空条件变量上等待的线程
public void offer(T t) {
lock.lock();
try {
while(queue.size() == capcity){
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(t);
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
带拒绝策略的插入任务方法put(MyRejectPolicy<T> rejectPolicy, T t)
可以看到,这个插入方法需要传入一个拒绝策略、队满则执行拒绝策略中的reject()方法
public void put(MyRejectPolicy<T> rejectPolicy, T t) {
lock.lock();
try {
if (queue.size() == capcity) {
//队满,执行拒绝策略
rejectPolicy.reject(this, t);
}else{
queue.offer(t);
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
实现线程池类
线程池的成员变量及构造方法
可以看到调用构造方法时除了给核心线程数赋值,还会初始化一个任务队列和一个拒绝策略。
// 任务队列
private MyBlockingQueue<Runnable> taskQueue;
//队列已满时的拒绝策略
private MyRejectPolicy<Runnable> rejectPolicy;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
/**
* 构造函数
*
* @param coreSize 线程池最大核心线程数
* @param queueCapcity 任务队列容量
* @param rejectPolicy 任务队列满时针对添加操作的拒绝策略
*/
public MyThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, MyRejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.taskQueue = new MyBlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}
实现一个Worker内部类
Worker内部类是对Thread进行包装,继承自Thread后重写了run方法
使得run方法的逻辑变为在执行已有任务后,会不断从队列中获取一个任务继续执行
只有任务队列中的任务也都执行完了,才结束线程,并在线程集合中删除自身
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task, String workerName) {
this.task = task;
this.setName(workerName);
}
@Override
public void run() {
while(task != null || (task = taskQueue.poll()) != null) {
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
workers.remove(this);
}
}
}
实现执行任务方法execute(Runnable task)
调用线程池的execute(Runnable task)方法即可提交任务给线程池执行
当任务数没有超过 coreSize 时,新建一个worker对象,把任务交给 worker 对象执行,并将worker加入线程集合,如果任务数超过 coreSize 时,加入任务队列暂存
// 执行任务
public void execute(Runnable task) {
synchronized (workers) {
if(workers.size() < coreSize) {
Worker worker = new Worker(task,"worker--"+workers.size());
workers.add(worker);
worker.start();
} else {
taskQueue.put(rejectPolicy, task);
}
}
}
测试
可以看到,在测试方法中,初始化了一个核心线程数和任务队列容量都为1的线程池threadPool ,拒绝策略是抛出异常,随后调用threadPool.execute()往线程池提交了3个任务,因为线程中调用sleep休息了1000毫秒,会导致第1个任务在被核心线程执行,第2个任务被放入阻塞队列,第3个任务到达后因为核心线程已满且阻塞队列也满了会执行初始化线程时提交的拒绝策略抛出异常
public static void main(String[] args) {
MyThreadPool threadPool = new MyThreadPool(
1,
1,
(queue, task)->{
throw new RuntimeException("任务执行失败 " + task);
});
for (int i = 1; i <= 3; i++) {
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}