现象
有一个数据迁移的 job,在本地测试的过程中直接 Hang 住了,半天没有反应。
通过 jstack 分析线程状态,发现大量处于 WAITING 状态的业务线程。
看了对应的代码后,也迅速把问题定位到了,下面主要分析问题主要发生的原因。
另外如果这个案例发生在生产环境并发较高的 ToC 接口,很可能造成大量接口响应超时,后果不堪设想(保不准真被开除了)。
线程池回顾
线程池的作用:统一管理线程资源,任务执行过程,达到线程资源复用的目的。
线程池处理任务过程:
- 判断当前线程数是否达到核心线程的阈值,如果没有,则创建线程执行当前任务
- 如果当前线程数达到核心线程阈值,则会尝试把当前任务放进任务队列,等待空闲线程去处理
- 如果任务队列也满了,则会尝试创建非核心线程执行当前任务
- 如果当前线程数达到最大线程数的阈值,则会触发拒绝策略
问题分析
核心原因:主任务和子任务使用同一线程池执行,最终造成线程池死锁,属于线程池使用不当造成的问题。
这个问题看似比较低级,但一不留心也非常容易犯,在部门 code review 的过程中就发现有人这么使用。
业务场景
在做商户门店数据迁移的时候,涉及的数据量比较大,因此将任务进行了如下拆分:
- 将全量门店拆分成一个个批次,每个批次包含 100 个门店,一个批次的处理定义为一个主任务
- 一个批次中再以单个门店为单位,拆分成一个个子任务
死锁问题
死锁出现的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
下面主要分析线程死锁出现的原因,问题发生的前提条件是:主任务一次性把核心线程都打满了,导致子任务无可用线程,只能先进入任务队列等待处理。
- 一个线程资源同一时间只能执行一个任务(即被一个任务使用),满足互斥条件
- 线程因执行任务被阻塞时,无法释放当前线程资源,满足占有且等待条件
- 当前任务未执行完前,线程无法被其它任务抢占,满足不可强行占有条件
- 子任务因为没有线程资源,导致一直待在任务队列中无法被执行;而主任务又因为子任务没有执行完而进入阻塞,无法释放持有的线程资源,满足循环等待条件
示例代码
核心代码如下(大家引以为鉴),外层任务
任务A中造成问题的代码
解决方案
- 线程池不使用阻塞队列,使用同步队列(这样可能会造成任务串行执行,达不到并发的效果)
- 父任务和子任务使用不同的线程池(采用的是此种解决方案)
- 控制父任务并发数低于核心线程数
通过这个事件分析,也警醒我们平时写代码的过程中要多思考每一行代码的含义,而不是简单地CV,为自己写下的每一行代码负责。