最近遇到一个使用 Apache HttpClient 过程中的问题,具体场景是
- 通过 Spring
@Scheduled(cron = "..")
方式执行定时任务 - 定时任务中并发使用 HttpClient 拉取数据
- 但是定时任务只会执行一次
- 因为 Spring 基于注解的定时任务,在非异步的情况的,上一次任务执行完之前不会执行下一个任务
- 所以怀疑是第一次执行的任务一直没有执行完,卡在了某个地方
还原场景
maven 依赖
1 | <dependency> |
程序简化后,代码如下
1 | package xyz.kail.demo.java.se.temp; |
正常情况下,以上程序会输出
countDownLatch.await();
httpClient.close();
executorService.shutdown();
但是多运行几次,会发现有时候会只输出 countDownLatch.await();
,程序会卡在 httpClient.close();
查看线程信息
1 | $ jcmd |
回忆一下线程状态
1 | package java.lang; |
根据 Thread.print 信息找到源码位置
AbstractConnPool.java:377
1 | // 入口 |
AbstractConnPool.java:207
1 |
|
可能原因分析
根据调用入口 大致可以确定 是 close 释放 HttpClient 资源的时候 和 execute 请求获取资源的时候 产生了死锁。
模拟可能的执行流程如下:
- 线程1:condition.signalAll()
- 线程2: 获取 this 锁
- 线程1:获取 this 锁 失败,BLOCKED
- 线程2:执行 getPoolEntryBlocking 方法
- 线程2:condition.wait (WAITING (parking))
- 最终两个线程 在互相等待对方释放锁/唤醒,产生死锁
分析到这基本上可以确定 应该是 httpcore 中 org.apache.http.pool.AbstractConnPool
这个类的Bug
如何解决
如果是 HttpClient (httpcore 模块) 的 Bug,可以看一下官方有没有修复,到 Github 官方仓库 httpcomponents-core 找到指定的文件 org/apache/http/pool/AbstractConnPool.java 查看 提交历史, Ctrl + F 搜索 关键字 fix
,最终找到了这次提交 HTTPCORE-446: fixed deadlock in AbstractConnPool shutdown code
点击这次提交 右侧的 <>
按钮(Browse the repository at this point in the history) 查看这次提交后的 git 仓库,发现修复之后 httpcore 的版本是 4.4.7-SNAPSHOT
。
升级到 httpcore maven 版本到 4.4.7+
后重试最初的代码,发现死锁问题已经解决,但是会抛出以下异常:
1 | org.apache.http.impl.execchain.RequestAbortedException: Request aborted |
如何避免
- 多线程并发使用共享资源的时候,如果不了解共享资源的内部机制,不了解是否存在并发问题的时候,一定要小心,如果不分析源码,最好也上网查一下相关的问题,如:”httpclient 并发问题” 等
- CountDownLatch 的使用方式也存在问题,比如这个示例程序中,countDownLatch.countDown() 写在了线程执行逻辑的第一行,真正的逻辑还没开始执行,就已经 countDown,实际上并没有起到相应的作用
- 如果确定共享资源存在并发问题,并且不确定官方有没有提供相应的解决方案的话,最快但不是最好的方式是:把共享资源放到线程内作为线程内部的资源,避免并发问题
- …
其它收获
- http-core 与 httpclient/httpmime 是分开两个仓库维护的,所以 maven 版本号不一定一致,但是 httpclient/httpmime 是同一个仓库下的两个模块,理论上版本号应该是一致的
- 使用httpclient必须知道的参数设置及代码写法、存在的风险
- 必须设置超时时间,否则可能在 网络 IO 上 卡死
- 默认重试3次机制
- 连接池管理
- 关于HttpClient重试策略的研究
- Apache HttpClient 资源释放、请求超时,导致线程池用光、内存不足
- HttpClient多线程并发问题