线程假死排查
saber599

背景

测试环境发现有部分业务执行一次后就不再执行了,出现了线程假死情况

排查过程

1. 查看日志

通过日志看到在备份文件时有异常发生,但是代码时将该异常捕获了,正常情况是不会导致线程假死的情况,需要进一步排查

2. 查看业务代码,先判断是否有明显bug

经排查后没发现明显bug

3. 将业务代码抽象出伪代码,本地debug查看原因

  1. 伪代码
    代码实现的业务功能为:将指定文件使用多线程挪到备份目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

@RunWith(JUnit4.class)
@Slf4j
public class BatchExecuteTest {
private final static ExecutorService FILE_PROCESS_WORKER = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

@Test
public void threadStopTest() {
try {
ArrayList<String> fileNames = Lists.newArrayList("test1.txt", "test2.txt", "test3.txt");
while (true) {
String dsType = "sftp";
String ip = "192.168.130.21";
String port = "22";
String username = "test";
String password = "test";
String connectPattern = "PORT";
FileHelper ftpHelper = FileManager.getFtpHelper("", dsType, ip, port, username, password, connectPattern);
batchExecute(fileNames, fileName -> {
try {
String sourcePath = Paths.get("/data1/upload/test", fileName).toString();
String targetPath = Paths.get("/data1/upload/test/bak", fileName).toString();
ftpHelper.move(sourcePath, targetPath);
} catch (Exception e) {
log.error("移动文件到失败", e);
}
});
ftpHelper.close();
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}

private <T> void batchExecute(List<T> processList, Consumer<T> consumer) throws InterruptedException {
if (CollectionUtils.isEmpty(processList)) {
return;
}
if (processList.size() == 1) {
consumer.accept(processList.get(0));
return;
}
CountDownLatch countDownLatch = new CountDownLatch(processList.size());
for (T t : processList) {
FILE_PROCESS_WORKER.execute(() -> {
try {
consumer.accept(t);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
}

}
  1. 使用idea本地debug查看堆栈信息
    截图

从图中能看出线程main一直卡在countDownLatch.await(),出现该情况可能会有两个原因

  1. 未正常计数,有task执行完毕后,没有进行countDown,导致没有一直减到0
  2. 有task一直未执行完,导致没有扣减数量

上面两个原因,从代码层面已经能排除第一个,因为在task执行体里使用了try
finally,只要task执行完,一定会contDown,所以极大可能是第二个原因,故而再看线程池中线程的堆栈

截图
截图

从图中能看出线程pool-1-thread-6一直卡在wait,从这堆栈信息就已经能看出问题所在了,原因在于FileHelper所封装的ChannelSftp不是一个线程安全的类,在多线程使用同一个对象的时候会出现,有的线程无法唤醒的情况。

问题修复

该问题可以考虑两种方式去修复

  1. 不使用多线程去move文件,就单线程循环move
  2. 修改FileHelper,将ChannelSftp放入到TreadLocal中,保证每个线程是一个单独的ChannelSftp,避免出现多线程竞争

综合业务场景,最终采用方案1,因为业务上就没有达到需要使用多线程去move的量,只是当时另一位同事刚好将move的逻辑放到了多线程处理中,所以采用方案一调整即可

思考

  1. 在使用CountDownLatch时,需要注意task因异常出现不结束的情况,会导致main线程假死
  2. 在新增业务代码时,需关注上下午,如果方法体存在并发,则需要根据实际情况考虑并发场景