原文:http://blog.kail.xyz/post/2019-04-21/tools/httpclient-lock.html
最近遇到一个使用 Apache HttpClient 过程中的问题,具体场景是
- 通过 Spring
@Scheduled(cron = "..")
方式执行定时任务
- 定时任务中并发使用 HttpClient 拉取数据
- 但是定时任务只会执行一次
- 因为 Spring 基于注解的定时任务,在非异步的情况的,上一次任务执行完之前不会执行下一个任务
- 所以怀疑是第一次执行的任务一直没有执行完,卡在了某个地方
还原场景
maven 依赖
1 2 3 4 5 6 7 8 9 10 11
|
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> <version>4.4.6</version> </dependency>
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.3</version> </dependency>
|
程序简化后,代码如下
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 57 58 59 60 61 62 63 64 65 66 67 68 69
|
package xyz.kail.demo.java.se.temp;
import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils;
import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
public class HttpClientMain {
public static void main(String[] args) throws IOException, InterruptedException {
int count = 20;
CountDownLatch countDownLatch = new CountDownLatch(count);
CloseableHttpClient httpClient = HttpClients.createDefault();
ExecutorService executorService = Executors.newFixedThreadPool(count);
for (int i = 0; i < count; i++) {
executorService.submit(() -> { countDownLatch.countDown(); // line num: 32 try { request(httpClient); } catch (IOException | InterruptedException e) { e.printStackTrace(); } });
}
countDownLatch.await(); // line num: 42 System.out.println("countDownLatch.await();");
httpClient.close(); // line num: 45 System.out.println("httpClient.close();");
executorService.shutdown(); System.out.println("executorService.shutdown();");
}
private static void request(CloseableHttpClient client) throws IOException, InterruptedException {
HttpGet request = new HttpGet("http://blog.kail.xyz"); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(1_000) .setSocketTimeout(5_000) .build();
request.setConfig(requestConfig); try (CloseableHttpResponse response = client.execute(request)) { // line num: 63 String data = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); } finally { request.releaseConnection(); } } }
|
正常情况下,以上程序会输出
countDownLatch.await();
httpClient.close();
executorService.shutdown();
但是多运行几次,会发现有时候会只输出 countDownLatch.await();
,程序会卡在 httpClient.close();
查看线程信息
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
|
$ jcmd ... 51051 xyz.kail.demo.java.se.temp.HttpClientMain
$ jcmd 51051 Thread.print ...
"pool-1-thread-20" #30 prio=5 os_prio=31 tid=0x00007fbb5b22c000 nid=0x6803 waiting on condition [0x0000700005997000] java.lang.Thread.State: WAITING (parking) # ❤❤❤❤ 关注 WAITING at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000076c0e7488> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) # ❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤ 关注 at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:377) at org.apache.http.pool.AbstractConnPool.access$200(AbstractConnPool.java:67) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:243) - locked <0x000000076de511b8> (a org.apache.http.pool.AbstractConnPool$2) at org.apache.http.pool.AbstractConnPool$2.get(AbstractConnPool.java:191) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:282) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:269) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:191) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:111) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) # ❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤ 关注 at xyz.kail.demo.java.se.temp.HttpClientMain.request(HttpClientMain.java:63) at xyz.kail.demo.java.se.temp.HttpClientMain.lambda$main$0(HttpClientMain.java:34) at xyz.kail.demo.java.se.temp.HttpClientMain$$Lambda$2/2137589296.run(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
...
"main" #1 prio=5 os_prio=31 tid=0x00007fbb5a802000 nid=0x1903 waiting for monitor entry [0x0000700003732000] java.lang.Thread.State: BLOCKED (on object monitor) # ❤❤❤❤ 关注 BLOCKED # ❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤ 关注 at org.apache.http.pool.AbstractConnPool$2.cancel(AbstractConnPool.java:207) - waiting to lock <0x000000076c88eb20> (a org.apache.http.pool.AbstractConnPool$2) at org.apache.http.pool.RouteSpecificPool.shutdown(RouteSpecificPool.java:155) at org.apache.http.pool.AbstractConnPool.shutdown(AbstractConnPool.java:152) at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.shutdown(PoolingHttpClientConnectionManager.java:396) at org.apache.http.impl.client.HttpClientBuilder$2.close(HttpClientBuilder.java:1225) at org.apache.http.impl.client.InternalHttpClient.close(InternalHttpClient.java:201) # ❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤❤ 关注 at xyz.kail.demo.java.se.temp.HttpClientMain.main(HttpClientMain.java:45)
...
|
回忆一下线程状态
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
|
package java.lang;
...
public class Thread implements Runnable {
...
public enum State { /** * 新创建了一个线程对象,但还没有调用start()方法 */ NEW,
/** * Thead.start */ RUNNABLE,
/** * 等待/阻塞 获取 synchronized 锁 */ BLOCKED,
/** * Object.wait / Thread.join / LockSupport.park 未设置超时时间 */ WAITING,
/** * 以下的几种情况 * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> */ TIMED_WAITING,
/** * 线程已经执行完成 */ TERMINATED; } }
|
Java线程的6种状态及切换
根据 Thread.print 信息找到源码位置
AbstractConnPool.java:377
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
// 入口 // xyz.kail.demo.java.se.temp.HttpClientMain.request(HttpClientMain.java:63) // at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) private E getPoolEntryBlocking(...){ ... this.lock.lock(); try { ... } else { ... // 5️⃣ 线程2 wait,但是这时候线程1已经 BLOCKED this.condition.await(); // line num: 377 [WAITING (parking)] success = true; } ... } finally { this.lock.unlock(); } } }
|
AbstractConnPool.java:207
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
|
@Override public Future<E> lease(final T route, final Object state, final FutureCallback<E> callback) {
...
return new Future<E>() {
... // 入口 // at xyz.kail.demo.java.se.temp.HttpClientMain.main(HttpClientMain.java:45) // at org.apache.http.impl.client.InternalHttpClient.close(InternalHttpClient.java:201) @Override public boolean cancel(final boolean mayInterruptIfRunning) { cancelled = true; lock.lock(); try { condition.signalAll(); // 1️⃣ 线程1 } finally { lock.unlock(); } // 3️⃣ 线程1 这时候线程2已经获取到锁,这里 BLOCKED synchronized (this) { // line num 207 BLOCKED (on object monitor) ... } }
...
@Override public E get(final long timeout, final TimeUnit tunit) throws InterruptedException, ExecutionException, TimeoutException { ... synchronized (this) { // 2️⃣ 线程2 获取锁 ... // 4️⃣ 线程2 执行 getPoolEntryBlocking 方法 final E leasedEntry = getPoolEntryBlocking(...); // 调用 377 行的代码 ... } }
}; }
|
可能原因分析
根据调用入口 大致可以确定 是 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 2 3 4 5 6 7 8 9 10 11 12
|
org.apache.http.impl.execchain.RequestAbortedException: Request aborted at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:194) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185) at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89) at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:111) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108) ... Caused by: java.lang.InterruptedException: Operation interrupted at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:384) ...
|
如何避免
- 多线程并发使用共享资源的时候,如果不了解共享资源的内部机制,不了解是否存在并发问题的时候,一定要小心,如果不分析源码,最好也上网查一下相关的问题,如:”httpclient 并发问题” 等
- CountDownLatch 的使用方式也存在问题,比如这个示例程序中,countDownLatch.countDown() 写在了线程执行逻辑的第一行,真正的逻辑还没开始执行,就已经 countDown,实际上并没有起到相应的作用
- 如果确定共享资源存在并发问题,并且不确定官方有没有提供相应的解决方案的话,最快但不是最好的方式是:把共享资源放到线程内作为线程内部的资源,避免并发问题
- …
其它收获
- 记一次HTTPClient模拟登录获取Cookie的开发历程
记一次HTTPClient模拟登录获取Cookie的开发历程 环境: springboot : 2.7 jdk: 1.8 httpClient : 4.5.13 设计方案 通过新建一个 ...
- 记一次httpclient Connection reset问题定位
问题:某业务系统在运行一段时间后,某个API一定概率偶现Connection reset现象. 问题定位: 首先想到的是要本地复现出这个问题,但一直复现不出来. 1.根据线上问题相关日志判断应该是有部 ...
- 记一个netcore HttpClient的坑
异常信息 The SSL connection could not be established, see inner exception ---> AuthenticationExceptio ...
- 记一次Windb死锁排查
正在开会,突然线上站点线程数破千.然后一群人现场dump分析. 先看一眼线程运行状态 !eeversion 发现CPU占用并不高,19%,937条线程正在运行. 看看他们都在干什么. ~* e !cl ...
- 9102年了,汇总下HttpClient问题,封印一个
如果找的是core的HttpClientFactory 出门右转. 官方写法,高并发下,TCP连接不能快速释放,导致端口占完,无法连接 Dispose 不是马上关闭tcp连接 主动关闭的一方为什么不能 ...
- update的where条件要把索引的字段带上,要不然就全表锁
update的where条件要把索引的字段带上,要不然就全表锁 文章目录 update的where条件要把索引的字段带上,要不然就全表锁 本文主要内容 背景 ...
- HttpWebRequest 改为 HttpClient 踩坑记-请求头设置
HttpWebRequest 改为 HttpClient 踩坑记-请求头设置 Intro 这两天改了一个项目,原来的项目是.net framework 项目,里面处理 HTTP 请求使用的是 WebR ...
- 在使用HttpClient做客户端调用一个API时 模拟并发调用时发生“死锁"?
平时还是比较喜欢看书的..但有时候遇到问题还是经常感到脑袋一蒙..智商果然是硬伤.. 同事发现了个问题,代码如下: class Program { static void Main(string[] ...
- 用gdb调试python多线程代码-记一次死锁的发现
| 版权:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接.如有问题,可以邮件:wangxu198709@gmail.com 前言 相信很多人都有 ...
随机推荐
- 面试题:什么叫B树
B是balanced的意思 是在二叉查找树 树的基础上增加了平衡的性质 导致它不是 二叉树了,变成了多叉树,但是叉几路又有了限制 否则怎么平衡呢 一棵m阶B树(balanced tree o ...
- 2019最新Android常用开源库总结(附带github链接)
前言 收集了一些比较常见的开源库,特此记录(已收录350+).另外,本文将持续更新,大家有关于Android 优秀的开源库,也可以在下面留言. 一 .基本控件 1.TextView HTextView ...
- YUM方法安装mysql5.7版本
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/kabolee/article/deta ...
- 将浏览器地址栏中的 Request 参数显示成中文
希望实现:在当 JSP 页面发起请求,或者 Servlet 跳转时,地址栏中的参数可以显示成中文. 在通常情况下,浏览器地址栏中的URL地址为了适配不同的浏览器,会将URL地址信息转码为"I ...
- 【转】在Keil uv5里面添加STC元器件库,不影响其他元件
先到网上下载stc.CBD(http://download.csdn.net/detail/mao0514/9699117) 还有STC新系列单片机的头文件,宏晶的网站就有 1.在Keil/C51/I ...
- pip问题:ImportError: cannot import name main
问题描述 今天使用pip安装python包的时候,提示可以升级到最新版的pip,然后就升级了pip,从8.1.1到19.0.3,结果,就出现了下面的问题,pip不能用了: Traceback (mos ...
- 基于gin框架搭建的一个简单的web服务
刚把go编程基础知识学习完了,学习的时间很短,可能还有的没有完全吸收.不过还是在项目中发现知识,然后在去回顾已学的知识,现在利用gin这个web框架做一个简单的CRUD操作. 1.Go Web框架的技 ...
- Vue基本用法
在学习Vue的基本用法之前,我们先简单的了解一些es6的语法 let: 特点:1.局部作用域 2.不会存在变量提升 3.变量不能重复声明 const: 特点:1.局部作用域 2.不会存在变量提升 3. ...
- MyBatis mapper.xml中SQL处理小于号与大于号
这种问题在xml处理sql的程序中经常需要我们来进行特殊处理. 其实很简单,我们只需作如下替换即可避免上述的错误: < <= > >= & ' " < ...
- 项目Beta冲刺(团队)--7/7
课程名称:软件工程1916|W(福州大学) 作业要求:项目Beta冲刺 团队名称:葫芦娃队 作业目标:进行新一轮的项目冲刺,尽力完成并完善项目 团队博客 队员学号 队员昵称 博客地址 04160242 ...