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

1. 引言

Hi,你好,我是有清

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

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

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

2. 排查过程

2.1. 接口问题排查

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

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

记一次线上使用线程池不当引起的线程卡死问题_第1张图片

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

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

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

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

2.2. 子线程任务未执行

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

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

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

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

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

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

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

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

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

2.3. 子线程代码排查

先看一波代码

记一次线上使用线程池不当引起的线程卡死问题_第2张图片

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

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

一图胜千言

记一次线上使用线程池不当引起的线程卡死问题_第3张图片

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

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

完整伪代码如下,感兴趣的同学可以看下 ```Java public static void main(String[] args) { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 2, TimeUnit.MINUTES, new LinkedBlockingQueue<>(7)); List > 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 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> futures = Lists.newArrayList();

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

        Future 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 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 操作,不阻塞主线程

你可能感兴趣的:(java,jvm,开发语言)