线程池死锁问题

2,398 阅读4分钟

现象

有一个数据迁移的 job,在本地测试的过程中直接 Hang 住了,半天没有反应。

通过 jstack 分析线程状态,发现大量处于 WAITING 状态的业务线程。

看了对应的代码后,也迅速把问题定位到了,下面主要分析问题主要发生的原因。

另外如果这个案例发生在生产环境并发较高的 ToC 接口,很可能造成大量接口响应超时,后果不堪设想(保不准真被开除了)。

线程池回顾

线程池的作用:统一管理线程资源,任务执行过程,达到线程资源复用的目的。

线程池处理任务过程:

  • 判断当前线程数是否达到核心线程的阈值,如果没有,则创建线程执行当前任务
  • 如果当前线程数达到核心线程阈值,则会尝试把当前任务放进任务队列,等待空闲线程去处理
  • 如果任务队列也满了,则会尝试创建非核心线程执行当前任务
  • 如果当前线程数达到最大线程数的阈值,则会触发拒绝策略

问题分析

核心原因:主任务和子任务使用同一线程池执行,最终造成线程池死锁,属于线程池使用不当造成的问题。

这个问题看似比较低级,但一不留心也非常容易犯,在部门 code review 的过程中就发现有人这么使用。

业务场景

在做商户门店数据迁移的时候,涉及的数据量比较大,因此将任务进行了如下拆分:

  • 将全量门店拆分成一个个批次,每个批次包含 100 个门店,一个批次的处理定义为一个主任务
  • 一个批次中再以单个门店为单位,拆分成一个个子任务

死锁问题

死锁出现的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

下面主要分析线程死锁出现的原因,问题发生的前提条件是:主任务一次性把核心线程都打满了,导致子任务无可用线程,只能先进入任务队列等待处理。

  • 一个线程资源同一时间只能执行一个任务(即被一个任务使用),满足互斥条件
  • 线程因执行任务被阻塞时,无法释放当前线程资源,满足占有且等待条件
  • 当前任务未执行完前,线程无法被其它任务抢占,满足不可强行占有条件
  • 子任务因为没有线程资源,导致一直待在任务队列中无法被执行;而主任务又因为子任务没有执行完而进入阻塞,无法释放持有的线程资源,满足循环等待条件

示例代码

核心代码如下(大家引以为鉴),外层任务

任务A中造成问题的代码

解决方案

  • 线程池不使用阻塞队列,使用同步队列(这样可能会造成任务串行执行,达不到并发的效果)
  • 父任务和子任务使用不同的线程池(采用的是此种解决方案)
  • 控制父任务并发数低于核心线程数

通过这个事件分析,也警醒我们平时写代码的过程中要多思考每一行代码的含义,而不是简单地CV,为自己写下的每一行代码负责。

推荐阅读