线程池参数千万不要这样设置,坑得我整篇文章都写错了,要注意!

转载 作者:来者不拒 更新时间:2024-01-31 09:23:56 24 4

你好呀,我是歪歪.

先给大家道个歉:

上周不是发布了这篇文章嘛:《三个烂怂八股文,变成两个场景题,打得我一脸懵逼。》 。

其中第一个关于线程池的场景,经过读者提醒可能有问题,我又一次用尽浑身解数分析了一波,发现之前确实分析的不对.

这个案例真的是再一次深入的刷新了我对于线程池运行过程的认知.

而由于我之前写过太多关于线程池的文章,对于线程池的运行过程太过于熟悉,基本熟悉到了源码信手拈来的地步.

所以我再次分析的时候,一度曾怀疑这个问题现象可能是 JDK 的 BUG,在 JDK BUG 库里面翻了一圈也没有发现有人提到过这个问题,我甚至想要发起这个问题.

最后阴差阳错的,还是定位到了问题的原因是线程池使用方面的问题,而问题的原因,最终说起来,极其简单,一点就透.

这一篇文章,歪师傅再次带大家盘一下这个问题.

问题再现

先给大家上代码:

这个问题最开始是一个读者提出来,发给我的一个 Demo,这个代码已经是我精简过的了.

这个代码运行起来会触发线程池的拒绝策略:

重点看一下我们的线程池定义:

private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(64, 64, 0, TimeUnit.MINUTES, new ArrayBlockingQueue<>(32)),

该线程池核心大小数和最大线程数都是 64,队列长度为 32,也就是说这个线程池同时能容纳的任务数是 64+32=96.

但是从代码可以看出,由于有 countDownLatch 的存在,可以确认 for 循环一次一定只会放 34 个任务进来.

JDK 线程池的运行原理,大家应该都是背的滚瓜烂熟了:先启用核心线程,然后任务进队列,如果队列满了,再启用最大线程数。最大线程数也满了,就触发拒绝策略.

那么按照我个人的理解,因为我们的核心线程数就是 64 个,已经完全大于 34 个任务了,所以线程池完全可以吃下这 34 个任务.

完全没有理由触发拒绝策略啊?

所以,我在之前的文章中给出的结论是:

线程池里面的任务执行完成了,核心线程就一定会释放出来等着接受下一波循环的任务,但是不会立马释放出来。从释放到就绪之间,有一个时间差的存在,导致线程池核心线程数不够用,从而导致触发拒绝策略.

老实说,这个结论从纯理论的角度来说,是真的有可能的。所以我才写了一篇文章去论证它.

而且我还通过重写线程池的 afterExecute 方法,延长了“核心线程收尾的时间”来确保问题复现.

也确实复现了.

但是很遗憾,这个结论在这个案例中是错误的.

之前的文章说了:

“线程池两个工作”和“主线程继续往线程池里面扔任务的动作”之间,没有先后逻辑控制.

我的验证方式是通过延长了“核心线程收尾的时间”来确保问题复现.

但是这里有两个条件,所以其实还有一个验证方式:让“主线程继续往线程池里面扔任务的动作”足够的慢,让线程池有足够的事件去收尾,这样问题就一定不会出现.

然而我忽略了这个验证方式,一心只是想着复现问题.

所以,当读者给我这样的一个代码片段的时候,我直接就是一整个愣住了:

他在主线程中睡了 2s,目的是为了让“主线程继续往线程池里面扔任务的动作”足够的慢:

如果按照我之前的推测,那么线程池是完全足够时间让线程就绪的.

我自己也进行了验证,而且我甚至把时间拉长到 10s,这样也确实是会触发拒绝策略:

看到这个运行结果的时候,我本能上是抗拒的,因为这一行代码的加入,运行结果和我预测的完全相反,相当于直接推翻了我前面的结论.

但是歪师傅写文章这么多年了,还是见过一些大场面的.

于是迅速开始思考原因.

最开始我怀疑这里面的 sleep 动作有问题,于是我直接改成了这样,相当于模拟线程空跑一趟,什么动作都没有做:

但是还是会抛出异常.

然后我又开始怀疑 CountDownLatch,于是我直接去掉了相关的代码,整个代码变成了这样:

public class MyTest {
    private static final ThreadPoolExecutor threadPoolExecutor =
            new ThreadPoolExecutor(64, 64,
                    0, TimeUnit.MINUTES,
                    new ArrayBlockingQueue<>(32));
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            Thread.sleep(100);
            for (int j = 0; j < 34; j++) {
                threadPoolExecutor.execute(() -> {
                    int a = 0;
                });
            }
            System.out.println("===============>  详情任务 - 任务处理完成");
        }
        System.out.println("都执行完成了");
    }
}

这个代码可以说已经非常简单了,除了线程池之外,没有其他的任何干扰项了.

但是,你直接粘过去跑,你会发现,还是会抛出异常:

核心线程数64,队列长度 32,每次往线程池里面扔 34 个任务,对应的任务完全没有任何耗时操作.

这样居然会触发线程池的拒绝策略?

又想起了几年前写文章时由于 idea “bug”遇到的诡异问题,甚至怀疑起了是“质子作祟”.

不知道你看到这里的时候有没有看出什么破绽,或者说新的思路.

反正我对着这份代码盯了一整天,调试了无数次,线程池的问题是真的难以调试,而且是在线程数比较多,没有排查思路的情况下,所以基本上没有什么进展.

峰回路转

事情的转机出现在我实在没有思路,然后开始重新复盘整个问题的时候.

再次翻看和提出这个问题的读者的聊天记录,这句话引起了我的注意:

解决问题的办法就是提高队列的容量.

我也不知道为什么,反正也没有思路,逮着个方向就顺便看看吧.

于是我直接把队列的长度从 32 提升到了 320:

程序立马就正常了: