CountDownLatch 遇到 线程池 卡死

今天早上突然 在钉钉告警群收到消息

想假装看不到,但是对面老大又在这个告警群里面,又不得不处理,于是我全局搜代码

找到代码位置

 //用户到期水军处理线程池
    private static final ThreadPoolExecutor custConsumeHandleThreadPool =
            new ThreadPoolExecutor(10,30,60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(200), new ThreadPoolExecutor.DiscardPolicy());

    @Override
    public void custConsumeHandle() {
            String lockKey = Constant.REDIS_KEY_PRE + "job:custConsumeHandle";
            //redis 锁 判断
            if (!RedisLock.get().lock(lockKey, "custConsumeHandle", 60 * 60)) {
             ////////////////就是这里告警了
                AlertContext.robot().alert("用户消费到期扫描->锁未释放,处理失败");
                return;
            }
            try {
                //查询商家列表
                List<Merchant> merchantList = this.merchantService.findAllMerchant();

                CountDownLatch countDownLatch = new CountDownLatch(merchantList.size());
                //循环对商家进行扣费
                for (Merchant merchant : merchantList) {
                    custConsumeHandleThreadPool.execute(() -> {
                        //  这里是业务处理,,,,我就不贴出来了
                        countDownLatch.countDown();
                    });
                }
                countDownLatch.await();
            } catch (Exception ex) {
                Logs.e(getClass(), "用户消费到期扫描->处理失败:", ex);
            } finally {
                //释放锁
                RedisLock.get().unlock(lockKey, "custConsumeHandle");
            }
    }

代码 也很清爽,这是一个定时器执行的,为了防止当前执行还没结束,又被定时器扫到了,于是加了个redis锁。

告警的位置正是 这个锁未释放,也就是finally代码都没执行!!!! finally 都没执行,只能考虑线程卡死在try 里面了。(肯定不是redis解锁的问题,因为redis锁是我写的,嘿嘿~~)

代码推理

try 里面能卡死的地方只有 countDownLatch.await() ,于是说明 countDownLatch.countDown(); 没有减减指定的数量。countDown 又是在线程池里面执行减减的,也就说明线程池任务没有执行完。

但是线程池总会执行完的,除非是线程满了且阻塞队列也满了,遭到了拒绝,于是我找了下日志,但是没有发现拒绝的错误日志。于是我仔细观察了线程池的构造:

 private static final ThreadPoolExecutor custConsumeHandleThreadPool =
            new ThreadPoolExecutor(10,30,60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(200), new ThreadPoolExecutor.DiscardPolicy());

这里指定的队列数就200,也就是说当任务有240的时候且任务执行很慢的时候,可能会导致任务被拒绝。

于是我查了下数据库 findAllMerchant 的量,发现有3百多个,我就不贴数据了(公司隐私)。

于是可以断定是线程池把任务抛弃了,然后观察了一下拒绝策略,好家伙,是这个 ThreadPoolExecutor.DiscardPolicy() 。

线程池的拒绝策略有下面几种:

RejectedExecutionHandler rejected = null;
rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常
rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败那么主线程会自己去执行该任务

DiscardPolicy 会直接丢弃任务,且不抛出异常,也就是默默的丢弃,难怪我搜不到拒绝的日志。。。。。

更加验证了我的猜想, 就是线程池直接抛弃了任务,导致 countDownLatch.countDown() 减减 不到 构造时候的值,从而造成阻塞,卡死。

大家还是慎用这个 DiscardPolicy 策略,很难找到问题的。

整改

调整了下线程池的队列大小到20000 ,实际量远远达不到。然后将拒绝策略改成默认的AbortPolicy 。然后上线,没问题了~~万事大吉!


强烈推荐一个 进阶 JAVA架构师 的博客

Java架构师修炼

本文完~

支付宝打赏 微信打赏

如果文章对您有帮助,您可以鼓励一下作者