likes
comments
collection
share

记一次线上使用线程池不当引起的线程卡死问题

作者站长头像
站长
· 阅读数 13

1. 引言

Hi,你好,我是有清

在金融系统中,业务人员可以通过页面点击来修改当前产品的保费利率,然后触发一系列的业务逻辑

近几天,业务人员反馈偶尔修改产品的保费利率页面会卡死,但是刷新页面后重试,往往都能成功....

红线要紧,下文中出现的代码,均为伪代码

2. 排查过程

2.1. 接口问题排查

在业务人员反馈问题发生的时间点前后,查看该接口的日志,的确存在大量超时的情况

最后的日志都停在了 countDownLatch 的 await 前

记一次线上使用线程池不当引起的线程卡死问题

这边结合上方伪代码,顺便科普一下 counrDownLatch 的用法

  • 初始化 countDownLatch,传入初始化参数,比如传入 3
  • 在 Task 任务中,使用 countDownLatch.countDown() 对 3 进行 -- 操作
  • 当 3 被子线程减到 0 的情况下,countDownLatch.await() 会自动被唤醒往下走,如果没有加超时时间线程会被持续夯住,如果加了超时时间,在时间到达之后则会进行异常抛出

那这个情况下,很显然 main 线程被阻塞住了,无法继续往下走,而且有意思的是 Task 中的日志均没有打印

那么即子线程中的任务都没有执行,没有进行 countDownLatch.countDown() 操作,导致可怜的 main 线程一直苦苦等待

2.2. 子线程任务未执行

那么为何子线程中的任务没有执行呢?

开始经典老八股了,线程池的工作流程是什么样的呢?请默写并背诵

  • 新任务进来,开启线程执行,直到核心线程数满载
  • 任务继续进来,核心线程数满载,将任务放到队列中,直到任务队列满载
  • 任务继续进来,任务队列满载,开启最大线程数执行,直到最大线程数满载
  • 任务继续进来,任务根据具体的拒绝策略,进行对应的处理

根据这个套路,我们的子任务未执行,可能的情况只有两个

  • 任务还在队列里,还没轮到他执行
  • 任务被抛弃了,根本没有执行到

但其实我们重写了拒绝策略,一旦任务被抛弃,我们会主动抛出出我们的业务异常,但是对日志进行关键字搜索,并没有发现该异常

那么真相只有一个,任务还在队列里,还没轮到他执行

但是很可惜,我们没有直接对线程池的可视化界面,但是我们可以借助 Arthas 的 watch 去佐证我们的判断,再对比正确机器和错误机器的线程队列大小之后,我们的推测是正确

那么,为什么子线程的任务不执行呢?我们继续看代码

2.3. 子线程代码排查

先看一波代码

记一次线上使用线程池不当引起的线程卡死问题

在子线程中,竟然又开了新的子线程去执行新任务

Main 线程开启 2 个 task 子线程,这 2 个子线程又开启 2 个 subTask 子线程

一图胜千言

记一次线上使用线程池不当引起的线程卡死问题

当主线程进来的时候,任务 1 开始执行,什么时候执行完成呢?必须等等 任务1-1 和 任务 1-2执行完成后,才能继续执行,但是这些任务队列还没满,有没有新的线程去处理这些任务,就导致线程池中任务一直无法被执行

概括一下就是:主线程等待 任务1,任务1 等待任务 1-1,1-2,但是1-1、1-2 无人执行,任务1 卡住,任务1卡住,任务 2 卡住,任务 1、2 卡住,主线程卡住

完整伪代码如下,感兴趣的同学可以看下

public static void main(String[] args) {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 2, TimeUnit.MINUTES, new LinkedBlockingQueue<>(7));
    List<Future<String>> futures = Lists.newArrayList();
    int taskCount = 2;
    CountDownLatch countDownLatch = new CountDownLatch(taskCount);
    int i = 1;
    while (i < taskCount) {
        Task task = new Task(countDownLatch, threadPoolExecutor, String.valueOf(i));
        Future<String> res = threadPoolExecutor.submit(task);
        futures.add(res);
        i += 1;
    }
    try {
        countDownLatch.await();
    } catch (Exception e) {

    }
}

static class Task implements Callable {

    private CountDownLatch countDownLatch;
    private ThreadPoolExecutor gocExecutors;
    private String taskId;


    public Task(CountDownLatch countDownLatch, ThreadPoolExecutor threadPoolExecutor, String taskId) {
        this.countDownLatch = countDownLatch;
        this.gocExecutors = threadPoolExecutor;
        this.taskId = taskId;
    }

    @Override
    public String call() throws Exception {

        Thread.sleep(300);

        int taskCounts = 2;

        CountDownLatch cl = new CountDownLatch(taskCounts);

        List<Future<String>> futures = Lists.newArrayList();

        for (int i = 1; i <= taskCounts; i += 1) {

            Future<String> res5 = gocExecutors.submit(new SubTask(cl, gocExecutors, String.valueOf(i), taskId));

            System.out.println("线程" + Thread.currentThread().getName() + "完成提交子任务,子任务编号为" + taskId + "__" + i);

            futures.add(res5);
        }

        cl.await(15, TimeUnit.SECONDS);

        for (Future<String> f : futures) {

            String fRes = f.get(15, TimeUnit.SECONDS);

            countDownLatch.countDown();

            System.out.println("线程" + Thread.currentThread().getName() + "计数器结束减一" + "父任务编号为" + taskId + ",计数器为" + countDownLatch.getCount());

            return "futures";
        }
        return "1";
    }

    //子任务
    static class SubTask implements Callable {

        private CountDownLatch countDownLatch;
        private ThreadPoolExecutor gocExecutors;
        private String taskId;
        private String parentId;


        public SubTask(CountDownLatch countDownLatch, ThreadPoolExecutor gocExecutors,
                String taskId, String parentId) {
            this.gocExecutors = gocExecutors;
            this.countDownLatch = countDownLatch;
            this.taskId = taskId;
            this.parentId = parentId;
        }

        @Override
        public String call() throws Exception {
            System.out.println("线程" + Thread.currentThread().getName() + "开始执行子任务,子任务编号为" + parentId + "__" + taskId);

            Thread.sleep(300);

            System.out.println("线程" + Thread.currentThread().getName() + "计数器开始减一,子任务编号为" + parentId + "__" + taskId + ",计数器为" + countDownLatch.getCount());

            countDownLatch.countDown();

            System.out.println("线程" + Thread.currentThread().getName() + "计数器结束减一,子任务编号为" + parentId + "__" + taskId + ",计数器为" + countDownLatch.getCount());
            return "2";
        }
    }

3. 问题解决

  • 父子任务采取不同的线程池
  • 拒绝策略中,进行 countDown 操作,不阻塞主线程
转载自:https://juejin.cn/post/7277173790935810067
评论
请登录