今天正好复习到线程池,几个参数看似简单,但是越想越觉得有交差和不解。新建线程池的方法如下,分别是(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler),通过这几个参数的"相互作用"来从新认识线程池的工作方式。
ThreadPoolExecutor executorService = new ThreadPoolExecutor(
1, 1,
1L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1),
new ThreadPoolExecutor.DiscardPolicy());//这里为了避免报错,使用DiscardPolicy,默认的Reject是AbortPolicy在队列已满时会报错
问题
首先我们提出这几个问题
- 如果core > max 会怎么样?
- 如果core > Queue.length 会发生什么?
- 如果max > Queue.length 线程池怎么处理?
- 如果core = max = Queue.length 线程池如何处理?
- 正常情况下的赋值。
测试
测试代码如下
public static void main(String[] args) {
ThreadPoolExecutor executorService = new ThreadPoolExecutor(
1, 1,
1L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1),
new ThreadPoolExecutor.DiscardPolicy());
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("start runnable 1 " + Thread.currentThread());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("runnable 1 finish " + Thread.currentThread());
}
});
for (int i = 2; i < 6; i++) {
final int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("start runnable " + finalI + " " + Thread.currentThread());
}
});
}
executorService.shutdown();
System.out.println("main finish ------ " + Thread.currentThread());
}
大家最好自己跑一下,有一个直观的认识,这里用表格表达的也不甚明了,其实如果用动图会好很多,不过自己太懒了=。=
-
线程队列递增
很明显的可以看出,线程池中只有 1 个线程,线程依次执行(并且是在线程1结束之后才开始的),队列越长能执行的线程越多,被舍弃的线程也就越少。
-
Max依次递增
结论:Max的值规定了线程池中线程的上限,其实并不只是有一个Core线程就只能跑一个线程,当Queue里面放不下的时候会开启非核心线程来跑这个『意外』的任务,而且与核心线程无关(这些线程不像图1中那样等待了线程1)
-
Core > Max
报错
-
Core依次递增(Core不能大于Max,所以Max也增加了)
结论:都在核心进程里面执行,和结论2类似
猜想
一直以来,我都是从字面上认为线程池的作用,Core即为能运行最多的线程量,Max就是超过Core需要排队的那一部分,而Queue在我的臆想一直都是无限的。这就造成了一个非常狭隘的思维,对设计者的意图没有思考,对底层的代码没有研究。
这里大家可以好好想一下线程池的工作方式,Core、Max与Queue的相互关系(想清楚这个,其他几个参数也能手到擒来)。
在这里我们从上面的结论再次猜想一下,Core自然是核心线程的数量,当核心线程没有满时,无论线程1是否闲置,都会创建一个新的线程,并且这些线程可以重用(这里就有一个存活时间的思考了);Max是线程池中可以存在的最大线程量,当超过Core线程数且小于Max时,这时线程池会新建一些线程来处理这些『来不及』处理的任务(来源于测试2),同时,Core不能大于Max(这是为什么呢?);作为一个队列,它担任着『缓存队列』的任务,像普通的队列一样,最多能存储多少就存储多少。
验证
当然是从源码角度
public void execute(Runnable command) {
if (command == null)// 1
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {// 2
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {// 3
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))// 4
reject(command);
else if (workerCountOf(recheck) == 0)// 5
addWorker(null, false);
} else if (!addWorker(command, false)) {
reject(command);// 6
}
}
首先,我觉得在看源码时应该明确自己的目的,这样在理解时更有针对性;其次,在阅读时一定不要纠结某一个地方,有时候看的代码成百上千行,不可能把每一个地方都看懂,比如在execute方法中就有位运算的使用、链表的操作还有addWorker一个更复杂的方法,不是说深究没有必要,而是去把这个方法当做某个变量去理解,我们开车是要前往目的地,而不是要了解车的构造。
列出几个方法的作用,更利于理解
ctl.get() //凡事ctl有关的操作其实都是位运算的使用,这里有兴趣的可以去查一下,并不难。
//这里我们只要知道,它像一个int一样,1代表一个状态,2代表另一个状态。
workerCountOf(c); //这里都是来取ctl保存的状态,就是字面意思上的,(Core线程)的运行数量
isRunning(c); //(线程)是否正在运行
addWorker(commad,boolean); //创建一个新线程(重要),boolean表示创建的是核心线程还是非核心线程
workQueue.offer(command) // 将线程加入到队列中(其实就是链表操作)
reject(command); // 执行拒绝策略
有了以上的准备,我们在理解时就比较容易了。
- 检查新线程是否为空。
- 获取线程池状态c,如果正在运行的Core线程小于预定值则创建一个新线程执行。(即使有空闲线程,也会创建新的)
- 如果已经超过了Core线程数,检查线程是否正在运行,同时加入线程队列成功。(如测试1)
- 双重检查,如果线程恰好执行完毕了,要从阻塞队列中移除该线程。
- 如果没有核心线程运行,创建非核心线程运行该任务。(如测试2)
- 没有加入到核心线程,创建非核心线程也失败了(如测试1)执行拒绝策略。
这里相信大家对线程池都有了一定的自己的理解了,有什么问题欢迎提出一起讨论进步。
2019年11月06日22:03:13 补充
int c = ctl.get();
// 1
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2
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);
}
// 3
else if (!addWorker(command, false))
reject(command);
可以从线程池的核心代码去理解
- 当没有超过Core Size时,始终会创建新线程。
- 当超过核心线程,但是可以加入到缓存队列时,不会创建新线程。如果当前线程池没有运行,会尝试启动(addWorker)。
- 当超过Core Size,同时Max Size有余量时,会尝试创建新线程,并且会在之后复用,否则执行Rejct策略。
这样即使参数再怎么变化,也能顺利的理解了。