背景
测试环境发现有部分业务执行一次后就不再执行了,出现了线程假死情况
排查过程
1. 查看日志
通过日志看到在备份文件时有异常发生,但是代码时将该异常捕获了,正常情况是不会导致线程假死的情况,需要进一步排查
2. 查看业务代码,先判断是否有明显bug
经排查后没发现明显bug
3. 将业务代码抽象出伪代码,本地debug查看原因
- 伪代码
代码实现的业务功能为:将指定文件使用多线程挪到备份目录
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(); }
}
|
- 使用idea本地debug查看堆栈信息
从图中能看出线程main一直卡在countDownLatch.await(),出现该情况可能会有两个原因
- 未正常计数,有task执行完毕后,没有进行countDown,导致没有一直减到0
- 有task一直未执行完,导致没有扣减数量
上面两个原因,从代码层面已经能排除第一个,因为在task执行体里使用了try
finally,只要task执行完,一定会contDown,所以极大可能是第二个原因,故而再看线程池中线程的堆栈
从图中能看出线程pool-1-thread-6一直卡在wait,从这堆栈信息就已经能看出问题所在了,原因在于FileHelper所封装的ChannelSftp不是一个线程安全的类,在多线程使用同一个对象的时候会出现,有的线程无法唤醒的情况。
问题修复
该问题可以考虑两种方式去修复
- 不使用多线程去move文件,就单线程循环move
- 修改FileHelper,将ChannelSftp放入到TreadLocal中,保证每个线程是一个单独的ChannelSftp,避免出现多线程竞争
综合业务场景,最终采用方案1,因为业务上就没有达到需要使用多线程去move的量,只是当时另一位同事刚好将move的逻辑放到了多线程处理中,所以采用方案一调整即可
思考
- 在使用CountDownLatch时,需要注意task因异常出现不结束的情况,会导致main线程假死
- 在新增业务代码时,需关注上下午,如果方法体存在并发,则需要根据实际情况考虑并发场景