问题描述
每次执行一组任务,一组任务最多有 15 个,多线程执行,每个线程处理一个任务;每次执行完一组任务后,再执行下一组,不存在上一组的任务和下一组一起执行的情况
代码重现
- 定义线程池
- 重现代码
@Test
public void contextLoads() throws Exception {
// 一共 10 批任务
for(int i = 0; i < 10; i++) {
// 每次执行一批任务
doOnceTasks();
System.out.println("---------------------------------------" + i);
}
}
/**
* 每次完成 15 个任务后,再进行下一次任务
* -->理论上 14 个核心线程+1 个阻塞队列即可完成一组任务,连非核心线程都无需使用,为什么会出现线程被占满的情况
*/
private void doOnceTasks() {
List<Future> futureList = Lists.newArrayListWithCapacity(15);
for(int i = 0; i < 15; ++i){
Future future = executor.submit(()->{
// 随机睡 0-5 秒
int sec = new Double(Math.random() * 5).intValue();
LockSupport.parkNanos(sec * 1000 * 1000 * 1000);
System.out.println(Thread.currentThread().getName() + " end");
// countDownLatch.countDown();
});
futureList.add(future);
}
// // 等待所有任务执行结束
for(Future future : futureList){
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
}
问题定位
- 错误堆栈信息展示
- 根据堆栈信息定位相应的代码
- 通过代码发现是因为超过最大线程数,触发拒绝策略
问题分析
- 为什么会创建非核心线程?
1.1 业务线程任务封装在Worker的firstTask属性,直接调用其run方法触发,但不会清除线程(见下第二批执行任务前的截图-14个线程处于阻塞等待)
(1)所以阻塞取业务线程结果,不会清除线程
1.2 Worker执行初始化的任务和循环取队列中的任务执行(见下runWorker截图)
1.3 第二批之后任务都是基于队列的生产与消费
(1)LinkedBlockingQueue#take 方法,如果队列已空,则所有取元素的线程会阻塞在一个 Lock 的 notEmpty 等待条件上;等有元素入队时,只会调用 signal 方法唤醒一个线程取元素,而不是所有线程
(2) 提交任务的速度比线程从阻塞队列取任务的速度快,进而导致创建非核心线程执行任务
解决方式
使用SynchronousQueue
- 使用 SynchronousQueue,即阻塞队列大小设置为 0
- SynchronousQueue 是根据是否有等待线程而决定是否入队成功,而 LinkedBlockingQueue 是根据缓冲区,而不管是否已经有等待线程
2.1 SynchronousQueue
2.2 LinkedBlockingQueue
根据业务情况配置阻塞队列
对于上述案例,因为任务最多只有15个,将阻塞队列大小设置为15,就保证了不会出现任务被拒绝
补充
ThreadPool ctl 变量的设计
- ThreadPool ctl变量是用int的高3位 + 29个0代表状态,用高位000+低29位来表示线程池中工作线程的数量
1.1 int COUNT_BITS = Integer.SIZE - 3 // 32 -3 =29
1.2 int CAPACITY = (1 << COUNT_BITS) - 1
(1)执行返回536870911(10进制)
(2)10进制转换为二进制(29个1)
怎样销毁线程(待下次详细分析)
- 实现思路:如果 getTask 返回null, 则结束循环,该 worker 线程将被销毁
- getTask 在什么情况下会返回 false?
2.1 如果线程池的状态为SHUTDOWN并且队列不为空
2.2 如果线程池的状态大于STOP
2.3 如果当前运行的线程数大于最大线程数
对提交任务分析(重点关注addWorker方法)
- addWorker前面流程简要概括
- CAS增加线程数
- 封装业务线程任务,启动线程