好玩的、不可思议的ThreadPool线程池
JDK1.5为我们新增了两种实现多线程的方式:线程池和Callable接口,本次我们来玩一下线程池。
首先,介绍一下ThreadPoolExecutor的构造函数及其7个参数的内容:
其次,我们来看一下线程池执行任务的流程:
ThreadPoolExecutor源码对execute方法的说明:
其本质就是ThreadPool线程池的执行任务流程,用流程图示意如下:
在单轮次的多个任务过来时,线程池中的线程执行任务时完全符合上述规律。
那么,如果我们在线程池执行完一轮任务后,再来一轮的任务呢?线程池中的现有线程(包括核心线程和非核心线程)该如何执行这一轮次的任务呢?
下面我写了代码进行测试,我把核心线程数设置为2个,最大可扩展线程数设置为4个,非核心线程数空闲生存时间设置为2s,阻塞队列设置为3个。
测试1:
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2, 4, 2, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
try {
for (int i = 0; i < 7; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "线程池");
});
}
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("=================第二轮线程==================");
for (int i = 0; i < 4; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "线程池");
});
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
测试结果如下:
按照常理来想,那么第一轮结束后有4个线程已经空闲下来了,那么第二轮任务过来时应该是这些空闲下来的任务直接接收任务,然后执行任务吧。但是测试结果呈现的显然不是我们预想的。
为了弄清楚其底层的原理,我们先来剖析一下,在第二轮任务来临时,线程池现有4个线程处于活跃状态,阻塞队列容量为3,现在处于空的状态。
第二轮有4个任务,但是线程只执行了3个任务,有1任务执行了拒绝策略。
明明线程池中现在有4个线程是处于活跃状态的,为什么不将4个任务全部执行,反而要将其中1个任务拒绝呢?
那么,我又进行了第二次测试。
测试2:
这次测试,我让非核心线程在空闲2秒后失效(其他代码不变),那么,线程池在即将执行第二轮任务时,线程池中只剩下核心线程(2个),并且阻塞队列(容量为3)也是空的。
将上述测试代码中的
TimeUnit.MILLISECONDS.sleep(500);
修改为:
TimeUnit.MILLISECONDS.sleep(2500);
然后执行,测试的结果如下:
本次测试,在第二轮任务来临时,线程池现有2个线程处于活跃状态,阻塞队列容量为3,而且处于空闲状态。
这次却将4个任务全部执行了。
原因分析
仔细查看源码及其文档说明后,也没有发现问题。
经过仔细思考,发现线程池的执行流程官方文档所写的是正确的(线程池的执行任务流程示意图表示的流程也是正确的)。因为当多个任务来临时,线程池不是首先判断是否有空闲的线程(execute方法根本就没有这个流程,这样的设计完全不符合人的思维习惯),而是直接判定线程数是否达到核心线程数,如果已达到,则进入阻塞队列,如果阻塞队列已满,再进行判定线程数是否已达到最大可扩展线程数,如果已达到则执行拒绝策略。
详细分析如下:
在测试1中即将执行第二轮任务时的线程池的状态如下:
线程池现有4个线程处于活跃状态,阻塞队列容量为3,现在处于空的状态。
当第二轮任务过来后,
1.判定现有线程数(目前是4)是否达到核心线程数(2),结果是已达到,进行下一步判定;
2.判定阻塞队列是否已满,目前任务是4个,阻塞队列的容量是3个,所以其中有3个任务放入了阻塞队列,另外1个任务需要进行下一步的判定;
3.判定现有线程数(目前是4)是否已达到最大可扩展线程数(4),结果是已达到,那么最后1个任务被执行拒绝策略。
在测试2即将执行第二轮任务时线程池的状态如下:
线程池现有2个线程处于活跃状态,阻塞队列容量为3,现在处于空的状态。
当第二轮任务过来后,
1.判定现有线程数(目前是2)是否达到核心线程数(2),结果是已达到,进行下一步判定;
2.判定阻塞队列是否已满,目前任务是4个,阻塞队列的容量是3个,所以其中有3个任务放入了阻塞队列,另外1个任务需要进行下一步的判定;
3.判定现有线程数(目前是2)是否已达到最大可扩展线程数(4),结果是未达到,那么线程池创建新的线程,并执行这最后1个任务。
那么我们得出结论:
线程池的工作流程如下:
1.判定现有线程数是否达到核心线程数,如果未达到,则创建线程并执行任务;
2.如果现有线程数已达到核心线程数,则判定阻塞队列是否已满,未满则进入阻塞队列;
3.如果阻塞队列已满,则判定现有线程数是否已达到最大可扩展线程数,如果未达到,则创建线程并执行任务;
4.如果现有线程数已达到最大可扩展线程数则执行拒绝策略。
官方文档的说明的和源码写的是一致的,经测试也没有问题,只是线程池的这种工作流程不符合人类的思维习惯。