关于线程池的基础知识,参考:java线程池ThreadPoolExecutor的原理及使用
这篇文章从实现的角度来实现两个版本的线程池,并对比java jdk中的ThreadPoolExecutor,看一下与工业级线程池的差距。
文章目录
关于线程池的四个思考
- 线程池刚刚创立,还没有Task到来的时候,池中的线程处于什么状态?
回答:如果是Runnable状态,那么cpu就可以调度其进行运行了,所以不能是就绪状态。
也不能是TIMED_WAITING状态,是处于waiting状态的,等待任务 - 当Task到来的时候,线程池中的线程如何得到通知?
回答:线程池中有一个阻塞队列,有任务放入阻塞队列,会执行notify,于是在锁池中的线程被唤醒,进行执行 - 当线程池中的线程完成工作,如何回到池中?
回答:实际上线程是去阻塞队列中取任务,如果没有任务,就再次进入waiting状态 - Task是什么东西?
回答:Task是自己定义的一个数据结构,不一定是Runnable对象。只要这个Task能够被你的线程识别就可以,在Task中要执行具体的业务。
实际上这4个问题是相互关联的,这四个问题就构成了线程池的实现思路,其关键就是要有一个阻塞队列。
左边可能有一大堆线程向里面put,右边是线程池中的线程从中取数据。
当6个格子已经满了,那么左侧的线程只能阻塞了
第一版线程池
Task接口
public interface Task{
public void execute();
}
ThreadPool
public class ThreadPool{
// 要持有一个阻塞队列
private BlockingQueue taskQueue = null;
// 持有一系列的线程
private List<WorkerThread> threads = new ArrayList<>();
// 是否停止线程池
private boolean isStopped = false;
public ThreadPool(int numOfThreads, int maxNumOfTasks){
taskQueue = new BlockingQueue(maxNumOfTasks);
for(int i = 0; i<numOfThreads; i++){
// 创建线程
threads.add(new WorkerThread(taskQueue));
}
for(WorkerThread thread: threads){
// 启动线程 等待调度 为Runnable状态
thread.start();
}
}
public synchronized void execute(Task task) throws Exception{
if(this.isStopped){
throw new IllegalStateException("ThreadPool is stopped");
}
// enqueue放入task到blockingqueue中
this.taskQueue.enqueue(task);
}
public synchronized void stop(){
this.isStopped = true;
for(WorkerThread thread: threads){
thread.doStop();
}
}
}
WorkerThread
public class WorkerThread extends Thread{
private BlockingQueue taskQueue = null;
// 这个线程是否停止
private boolean isStopped = false;
public WorkThread(BlockingQueue queue){
// 比如现在阻塞队列中来了一个任务,线程1被唤醒,去执行,执行完成后,再继续while循环中,如果有值,就执行,否则就进入阻塞状态
while(!isStopped()){
try{
// Runnable --> Waiting,当前线程就会被阻塞
// 比如上面启动了5个线程,5个线程执行到这里,就都进入了阻塞状态
Task task = (Task)taskQueue.dequeue();
task.execute();
}catch(Exception e){
// log or otherwise report exception
// but keep pool thread alive
}
}
}
public synchronized void doStop(){
isStopped = true;
// break pool thread out of dequeue() call
this.interrupt();
}
public synchronized boolean isStopped(){
return isStopped;
}
}
关键的代码
taskQueue.dequeue(); 于是当前线程就会被阻塞
比如上面启动了5个线程,5个线程执行到这里,就都进入了阻塞状态。
比如现在阻塞队列中来了一个任务,线程1被唤醒,去执行,执行完成后,再继续while循环中,如果有值,就执行,否则就进入阻塞状态
线程池停止
通过interrupt打断waiting,在execute方法中是无法打断的
下一次while循环的时候,发现isStop=true,就停止了,所以线程池停止是会把当前的任务都执行完成之后再停止。
JDK线程池中的实现1:核心执行流程
我们看一下ThreadPoolExecutor中实现的核心执行流程
public class ThreadPoolExecutor{
private final BlockingQueue<Runnable> workQueue;
private final ReentrantLock mainLock = new ReentrantLock();
private final HashSet<Worker> workers = new HashSet<Worker>();
private final Condition termination = mainLock.newCondition();
public void execute(Runnable command){
// 这里涉及到corePoolSize maxPoolSize的逻辑 为了和上面做对比,这块先省略
addWorder(command);
}
private boolean addWorker(Runnable firstTask){
Worker w = null;
w = new Worker(firstTask);
final Thread t = w.thread;
workers.add(w);
// 这里会启动线程,进入到Runnable状态
t.start();
}
public void tryTerminate(){
termination.signalAll();
}
private final class Woker implements Runnable{
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask){
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run(){
// delegate runWorker method
runWoker(this);
}
final woid runWorker(Worker w){
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
// while循环 等getTask()==null的时候,其就会进入到waiting状态
while(task!=null || (task=getTask())!=null){
task.run();
}
}
private Runnable getTask(){
Runnable r = timed?
workQueue.poll() : workQueue.take();
}
}
}
当执行
threadExecutor.execute(new LiftOffRunnable());
threadExecutor.execute(new LiftOnRunnable());
其执行流程是:
-
执行execute方法,会创建Worker,Worker class中含有两个成员变量Thread thread和Runnable firstTask,将创建的Worker实例加入到blockingQueue中,将thread启动
-
thread启动后,执行run方法,delegate to runWorker方法,其在一个while循环中获取queue中的Worker实例,获取到了,就执行run方法,如果没有获取到,就进入waiting状态
可以看到这两者的核心思路是相同的。
第二版线程池
参考:gitee链接
这是一个相对完整的实现,包括coreThreadSize,maxThreadSize,任务拒绝策略,keepalive时间等
一个完整的线程池应该具备:
-
任务队列,用来缓存提交的任务,LinkedRunnableQueue
-
queueSize,任务队列主要存放提交的Runnable,为了防止内存溢出,需要限制任务数量
-
线程数量管理,init->core->max
-
任务拒绝策略
-
线程工厂:用于个性化定制线程,比如将线程设置为守护线程以及设置线程名称
-
keepalive时间:该时间主要决定线程各个重要参数,如initSize,coreSize,maxSize,自动维护线程池线程数量的采样时间间隔
类图
这里面线程池使用者提交的是Runnable,但是在线程池内部使用的是InternalTask,这个地方有些绕
-
创建BasicThreadPoolTask,wangwenjun.threadpool.BasicThreadPoolTask#init中其会启动自身线程,并且启动initSize的两个线程my-thread-pool-exec-0及exec-1
public BasicThreadPoolTask(int initSize, int maxSize, int coreSize, int queueSize) { this.init(); } private void init() { // 将自身线程启动起来 start(); }
-
在启动两个线程的时候,wangwenjun.threadpool.BasicThreadPoolTask#newThread中,实际上BasicThreadPoolTask 线程池中 每个线程执行的任务都是InternalTask,然后把thread和InternalTask 组合成为ThreadTaskWrapper对象,将这个ThreadTaskWrapper 放到threadQueue这个ArrayDeque里面
-
此时两个启动的线程就都执行了InternalTask的run方法,阻塞在runnableQueue.take()这里
为什么要使用InternalTask?原因就在这里,如果BasicThreadPoolTask持有的LinkedRunnableQueue中放的都是Runnable,那么线程去取的时候就直接执行完成了(调用者线程是不知道queue存在的,所以没办法再调用者线程中做阻塞),没办法做阻塞了,所以需要InternalTask来实现线程阻塞
-
当线程池使用者提交了Runnable,其执行wangwenjun.threadpool.BasicThreadPoolTask#execute方法,调用LinkedRunnableQueue#offer方法,而此时阻塞在runnableQueue.take()的线程满足条件,开始执行Runnable的run方法
-
当销毁线程池的时候,需要
threadTask.internalTask.stop(); threadTask.thread.interrupt();
为了支持这两个操作 需要持有这两个对象,于是采用了ThreadTask,并且使用了ArrayDeque数据结构
BasicThreadPoolTask内部结构总结
这里面实现了线程池中工作线程数量的扩容和缩容。
使用了jvisualvm来查看线程状态
下图是线程池刚刚启用的时候,由initSize 2 threads -->coreSize 4 threads
thread完成任务的时间是5秒,打印采样的间隔也是5秒,维护线程池线程数量的采样间隔是10秒
所以28:16秒创建两个线程,执行两个任务ThreadTask
28:21,由于线程池线程数量的采样间隔是10秒,所以仍旧是2个线程,执行两个任务,所以剩下的任务数量是16个
而28:26,其扩容了线程池,于是4个线程取到了4个任务
28:31,应该就剩下12个任务了
28:36,应该就剩下8个任务了,并且再度扩容,4个线程扩容到6个线程
28:41,应该就剩下两个任务了,6个线程中pool-3和pool-2抢到了这两个任务
28:46,所有任务完成,此时又进行线程数量检测,线程池缩容
private final Queue<ThreadTask> threadQueue = new ArrayDeque<>();
这里选择的数据结构是ArrayDeque
public E remove() {
return removeFirst();
}
由于pool-0和pool-1线程都阻塞在take()处,所以需要interrupt
"thread-pool--1" #32 prio=5 os_prio=0 tid=0x0000000020d34800 nid=0x3b18 in Object.wait() [0x00000000254ee000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at wangwenjun.threadpool.LinkedRunnableQueue.take(LinkedRunnableQueue.java:43)
- locked <0x000000076be3ec70> (a java.util.LinkedList)
at wangwenjun.threadpool.InternalTask.run(InternalTask.java:25)
at java.lang.Thread.run(Thread.java:748)
关闭掉这两个线程之后,线程数量=4=coreSize
最后调用shutdown,先关闭掉线程池中所有的线程,最后把BasicThreadPoolTask也关闭掉
JDK线程池中的实现2:工作线程数量的扩容和缩容
JDK中的ThreadPoolExecutor,关于keepaliveTime以及线程数量的检测,其并没有如我们第二版设计的方式,将自身设置为一个线程来进行检测和操作
扩容
ThreadPoolExecutor#execute方法
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
工作线程数量扩容过程总结
workQueue是一个BlockingQueue,也就是说当前线程数小于corePoolSize,那么就启用新的线程,如果任务数大于corePoolSize,那么就放到这个workQueue里面(workQueue.offer)。如果workQueue已经满了,那么就启动新的线程,直到maxPoolSize,如果还有新的任务过来,就执行reject policy
对比:
缩容
是在java.util.concurrent.ThreadPoolExecutor#runWorker的时候
try{
while (task != null || (task = getTask()) != null) {
task.run();
}
}
finally {
processWorkerExit(w, completedAbruptly);
}
在getTask()的时候,其使用keepalive参数,通过queue.poll(),即如果某个线程没有在keepAliveTime时间内返回为null,那么其就不会执行到processWorkerExit方法
但如果超时了仍然两手空空没拉到活,表明这个临时线程太闲了,这个线程会被销毁回收。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
通过processWorkerExit方法来回收线程
private void processWorkerExit(Worker w, boolean completedAbruptly) {
workers.remove(w);
}
JDK线程池中的实现3:关闭线程池
shutdown方法
java.util.concurrent.ThreadPoolExecutor#shutdown
interruptIdleWorkers();
java.util.concurrent.ThreadPoolExecutor#interruptIdleWorkers(boolean)
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
}
}
}
shutdown(); 是一个非阻塞方法
- 停止接收外部submit的任务
- 内部正在跑的任务和队列里等待的任务,会执行完
- interruptIdleWorkers 打断idle threads
- 当正在执行的线程变为idle状态,才真正停止
awaitTermination方法
如果想阻塞,在线程池任务结束之后执行某些代码,可以使用awaitTermination方法
Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted, whichever happens first.
当前线程阻塞,直到
- 等所有已提交的任务(包括正在跑的和队列中等待的)执行完
- 或者等超时时间到
- 或者线程被中断,抛出InterruptedException
当线程正在执行的时候,其就会block住,从下面的代码可以看出,其timeout的作用仅仅是看是否在规定的时间内完成了所有线程都为Terminated状态,如果有,则返回true,否则返回false
for (;;) {
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
if (nanos <= 0)
return false;
nanos = termination.awaitNanos(nanos);
}
下面这种使用方式,对于所执行线程可以被打断是有效的
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.HOURS);
// executorService all worker threads exit
// do something;
setDaemon+awaitTermination
如果不调用shutDown方法,由于ThreadPoolExecutor中有非daemon的thread(默认ThreadFactory daemon = false),所以方法不会执行结束,jvm不会退出
如果所执行不可以被打断(如访问db或者访问网络,很慢,但是无法中断),那么怎么在规定时间结束线程池呢?
如果想使用awaitTermination方法,就是在规定的时间完成,可以通过ThreadPoolExecutor的ThreadFactory setDaemon来实现
shutdownNow方法
shutdownNow(); 也是一个非阻塞线程
- 先停止接收外部提交的任务
- interrupt all workers thread 尝试将正在跑的任务interrupt中断(测试代码因为是sleep方法,支持中断,如果是不支持中断的,如访问db或者访问网络,很慢,但是无法中断)
- drainQueue方法 return list<Runnable> in queue(忽略队列里面等待的任务),可以返回未执行的任务列表
- 线程池中所有线程退出
停止线程池测试代码参考:gitee链接
JDK线程池中的实现4:任务拒绝策略
-
java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
A handler for rejected tasks that silently discards the rejected task. 静默丢弃,根本不知道发生了什么
-
java.util.concurrent.ThreadPoolExecutor.AbortPolicy
A handler for rejected tasks that throws a {@code RejectedExecutionException}. 抛出异常
-
java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
A handler for rejected tasks that runs the rejected task directly in the calling thread of the {@code execute} method, unless the executor has been shut down, in which case the task is discarded.
比如tomcat线程调用线程池,被拒绝后直接使用tomcat线程来执行
-
java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy
A handler for rejected tasks that discards the oldest unhandled request and then retries {@code execute}, unless the executor is shut down, in which case the task is discarded.
处理被拒绝任务的处理程序,它丢弃最旧的未处理请求,,建议不要使用,根本不知道发生了什么
线程执行中RuntimeException错误处理
executorService#execute中发生了错误RuntimeException,如何处理?
注意execute runnable是没有返回值的
方法1:自定义ThreadFactory使用uncaughtExceptionHandler
但是这种方式并不推荐,因为只能拿到thread和throwable cause
但是拿不到Runnable,所以比如失败之后你想做一些补偿,比如再试一次。
或者成功的话,更新数据库某个字段;如果失败了,同样更新数据库某个字段,其拿不到Runnable中的信息。
方法2:采用thread monitor 模式来处理
将任务代码和监控代码写在了一起
方法3:自定义ThreadPoolExecutor
参考:gitee链接
我的思考,对线程池任务是否需要持久化处理?
比如说我们重启服务对线程池的影响。
场景1:kill pid,此时要关闭线程池,
使用shutdown,非阻塞的,不好。
shutdown+awaitTermination指定时间,也可能有失败。
shutdownNow,要丢弃队列中的任务,也不好吧。
是不是需要把未执行完成以及workQueue中丢弃的任务记录下来,否则可能会出现一些不一致的情况。
场景2:kill -9 pid,此时线程池任务以及未执行完成的任务就全部都丢失了,内核来清理资源了。
场景3:线程执行过程中发生RuntimeException,怎么办?
线程池设计上使用了命令模式
比如我们自己创建Thread对象来执行Runnable,就是没有使用命令模式
在java多线程中,可以使用Executor来代替显式地创建Thread对象,这就是使用了命令模式
这里的Command模式解析
HashSet<Worker>就类似于执行者集合,execute方法就是发出执行的命令,这里的执行命令是借助于blockingQueue自动实现的,最终调用runWorker方法
命令的执行者Receiver是Worker
这里并不是Command关联Receiver,而是Receiver中持有了Command,来进行执行
把Runnable当做是Command,LiftOffRunnable和LiftOnRunnable作为Command的实现
就是把request(LiftOffRunnable和LiftOnRunnable)封装起来(用worker),把调用者(ExecutorService)和执行者(Worker中的thread)进行解耦
当我们调用时,执行的时序首先是ThreadPool(Invoker类),然后是Worker类(Receiver类),最后是Concrete Command类。也就是说一条命令的执行被分成了三步,它的耦合度要比把所有的操作都封装到一个类中要低的多(直接使用一个Thread即Receiver来执行Concrete Command),而这也正是命令模式的精髓所在:把命令的调用者Invoker与执行者Receiver分开,使双方不必关心对方是如何操作的。