Java 手写一个线程池 - SimpleExecutor
b站视频讲解:
- https://www.bilibili.com/video/BV1jU4y1K7Tr/
- https://www.bilibili.com/video/BV1rF411z76U/
什么是线程池 ?
线程池可以简单理解为一个加工厂,里面有一定数量的加工机器。该加工厂可以执行各种加工任务,加工厂里面会有一定数量的加工机器一直运作,剩余的机器按照
需要执行的任务量来动态的启动和关闭。
这里的加工厂就是一个线程池,加工厂里面的机器就可以理解为一个线程,至少处于运行的机器的数量为该线程池的核心线程数量,线程池总是保证里面至少有
核心线程数量个线程处于运行状态,可以随时执行新的任务。
为什么需要线程池 ?
我们知道,一个线程的创建需要经过很多的准备工作,比如向系统申请线程运行需要的内存空间等,这些一般是需要进行系统调用的,相应的代价比较大,
线程池可以很好地复用一定数量的线程,使其可以不断的执行新的任务,而不需要频繁地进行线程的创建,减少因创建线程而带来的系统开销,也可以提高任务的
执行效率。
线程池的运行原理
线程池里面有一个存放任务的阻塞任务队列,以及一个存放工作线程的工作线程集合。线程池启动时,并不会创建新的线程,每当有新的任务进入线程池时,如果
当前已经启动的线程数量没有达到最大的线程数量限制,会将该任务与一个线程包装成一个以当前新任务为首任务的工作线程对象并启动,当一个工作线程完成一个
任务后,会从线程池中的阻塞任务队列中去获取一个新的任务,如果没有获取到,任务队列会将核心线程阻塞,非核心线程返回结束。
注意:线程池里面的核心线程不是一成不变的,简单讲,假设线程池里最多允许有10个工作线程,编号1到10,5个核心线程,那核心不一定都是第1到5号工作线程,原因是
每个工作线程的任务结束时刻是不一致的,而且判断一个工作线程是否是核心线程,是根据当前线程池中的实际的工作线程数量来判断的,所以线程池中至少会有
核心线程数量个工作线程处于运行中,可以随时执行任务,而每个核心线程是会发生变化的。
线程池的关键参数以及属性
线程池的状态
在线程池的JDK实现中,线程池对应的状态比较复杂:
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
- 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 所有的任务都已经完成
- TERMINATED: terminated() has completed The numerical order among these values matters, to allow ordered comparisons 线程池结束
而且JDK的状态是通过一个整型的数来存储的,高3位表示线程池的状态,低29位表示线程池中的线程数量,通过位运算来读取。
这里我们就简单点,线程池就两个状态,RUNNING 、 STOPPED.用两个int类型的常量来表示。
具体实现
可以按照两个流程来剖析
- 1.用户提交新的任务的流程
- 2.工作线程执行任务的流程
工作线程对象
private final class Worker implements Runnable {
// 首任务
Runnable firstTask;
// 线程
Thread thread;
// 当前工作线程已经完成的任务数量
int finishedTask = 0;
// 构造函数
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = new Thread(this);
}
// 工作线程run方法
@Override
public void run() {
runWorker(this);
}
}
用户提交新的任务
public void execute(Runnable task) throws RejectTaskException {
if (task == null) {
throw new NullPointerException("The task can not be null");
}
// 检查当前的线程池是否为RUNNING状态
if (State.get() == RUNNING) {
// 接收任务,1:直接创建一个worker,当前的task作为worker的firstTask即可
// 2:worker数量已经达到最大限制了,我们不能再次创建新的worker,需要往任务队列里面去放
if (WorkerCount.get() < corePoolSize && addWorker(task, true)) {
return;
}
if (WorkerCount.get() < maxPoolSize && addWorker(task, false)) {
return;
}
// 只有往任务队列里面放了
if (State.get() == RUNNING) {
if (!TaskQueue.offer(task)) {
throw new RejectTaskException();
}
}
} else {// 说明当前线程池处于停止状态了,拒绝任务
throw new RejectTaskException("线程池已经停止,拒绝添加新的任务");
}
}
这个流程是和工作线程执行任务的流程是分开并发进行的,可以看到往线程池中添加一个任务流程是比较简单的,可以概括为:
首先检查任务是否为空,然后判断当前线程池的状态,根据状态决定是否拒绝,一旦可以接收任务,随即判断当前线程池中的工作线程数量是否达到了核心线程数量,
达到即创建核心线程,反之如果没有达到最大工作线程数量限制,则创建非核心线程,反之将任务添加到任务队列中。
工作线程执行任务
runWorker()
这是工作线程的工作流程方法,首先会去执行worker本身的首任务,执行完成,然后不断去任务队列中去获取新的任务,当从任务队列中获取不到任务时,
非核心线程会返回null,随即进入了工作线程的正常退出逻辑,反之为核心线程时,getTask()方法会将该工作线程阻塞起来直到有新的任务进入任务队列。
private void runWorker(Worker worker) {
if (worker == null) throw new NullPointerException();
// Thread wt = Thread.currentThread();
Thread wt = worker.thread;
Runnable task = worker.firstTask;// worker的首任务,这个首要任务会在后续被执行
worker.firstTask = null;
try {
while (task != null || (task = getTask()) != null) {
if (wt.isInterrupted()) {
System.out.println("This task is interrupted");
return;
}
if (State.get() == STOPPED) {
System.out.println("The thread pool has already stopped.");
return;
}
task.run();
task = null;
worker.finishedTask++;
}
} finally {
// worker的正常退出逻辑
L.lock();
try {
workers.remove(worker);
if (casDecreaseWorkerCount()) {
finishedTaskCount += worker.finishedTask;
}
} finally {
L.unlock();
}
}
}
getTask()
这部分的逻辑也比较简单,根据前来获取任务的工作线程的类型来判断拿不到任务时应该采用的策略,非核心线程直接在timeout后返回null,核心线程则是被挂起阻塞直到
有新的任务加入到任务队列中。
public Runnable getTask() {
if (State.get() == STOPPED) return null;
Runnable task = null;
while (true) {
try {
if (State.get() == STOPPED) return null;
// 当前线程池中的线程数量 < corePoolSize => 核心线程, 否则就是非核心线程
// 以核心线程的角色去获取任务
if (WorkerCount.get() <= corePoolSize) {
// core
task = TaskQueue.take();
} else {// 非核心线程
task = TaskQueue.poll(keepAliveTime, TimeUnit.MILLISECONDS);
}
if (task != null) {
return task;
}
} catch (InterruptedException interrupt) {
return null;
}
}
}