原文: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 死锁问题的更多相关文章

  1. 记一次HTTPClient模拟登录获取Cookie的开发历程

    记一次HTTPClient模拟登录获取Cookie的开发历程 环境: ​ springboot : 2.7 ​ jdk: 1.8 ​ httpClient : 4.5.13 设计方案 ​ 通过新建一个 ...

  2. 记一次httpclient Connection reset问题定位

    问题:某业务系统在运行一段时间后,某个API一定概率偶现Connection reset现象. 问题定位: 首先想到的是要本地复现出这个问题,但一直复现不出来. 1.根据线上问题相关日志判断应该是有部 ...

  3. 记一个netcore HttpClient的坑

    异常信息 The SSL connection could not be established, see inner exception ---> AuthenticationExceptio ...

  4. 记一次Windb死锁排查

    正在开会,突然线上站点线程数破千.然后一群人现场dump分析. 先看一眼线程运行状态 !eeversion 发现CPU占用并不高,19%,937条线程正在运行. 看看他们都在干什么. ~* e !cl ...

  5. 9102年了,汇总下HttpClient问题,封印一个

    如果找的是core的HttpClientFactory 出门右转. 官方写法,高并发下,TCP连接不能快速释放,导致端口占完,无法连接 Dispose 不是马上关闭tcp连接 主动关闭的一方为什么不能 ...

  6. update的where条件要把索引的字段带上,要不然就全表锁

    update的where条件要把索引的字段带上,要不然就全表锁 文章目录 update的where条件要把索引的字段带上,要不然就全表锁        本文主要内容        背景        ...

  7. HttpWebRequest 改为 HttpClient 踩坑记-请求头设置

    HttpWebRequest 改为 HttpClient 踩坑记-请求头设置 Intro 这两天改了一个项目,原来的项目是.net framework 项目,里面处理 HTTP 请求使用的是 WebR ...

  8. 在使用HttpClient做客户端调用一个API时 模拟并发调用时发生“死锁"?

    平时还是比较喜欢看书的..但有时候遇到问题还是经常感到脑袋一蒙..智商果然是硬伤.. 同事发现了个问题,代码如下: class Program { static void Main(string[] ...

  9. 用gdb调试python多线程代码-记一次死锁的发现

    | 版权:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接.如有问题,可以邮件:wangxu198709@gmail.com 前言 相信很多人都有 ...

随机推荐

  1. 阿里熔断限流Sentinel研究

    1. 阿里熔断限流Sentinel研究 1.1. 功能特点 丰富的应用场景:例如秒杀(即突发流量控制在系统容量可以承受的范围).消息削峰填谷.集群流量控制.实时熔断下游不可用应用等 完备的实时监控:S ...

  2. 单词canutillos祖母绿canutillos英语

    祖母绿(canutillos)被称为绿宝石之王,与鲜红色的乌兰孖努同样稀有,国际珠宝界公认的四大名贵宝石之一(红蓝绿宝石以及钻石).因其特有的绿色和独特的魅力,以及神奇的传说,深受西方人的青睐. 祖母 ...

  3. TP5.0使用助手函数model出现\common\Model\类不存在

    在ThinkPHP5.0中有一个助手助手函数model(),可以实例化具体的模型,包括分层模型,只要传入类名(第一个参数),分层名(第二个参数).这个函数其实是ThinkPHP框架Loader中的一个 ...

  4. 初探PHP设计模式

    设计模式不是一套具体的语言框架,是行之有效的编码规范,是一套被反复使用.多数人知晓的.经过分类的.代码设计经验的总结.使用设计模式的目的:为了代码可重用性.让代码更容易被他人理解.保证代码可靠性.合理 ...

  5. Mysql 单表查询-排序-分页-group by初识

    Mysql 单表查询-排序-分页-group by初识 对于select 来说, 分组聚合(((group by; aggregation), 排序 (order by** ), 分页查询 (limi ...

  6. python将科学计数法表示的数值的字符串转换成数值型数据

    今天碰到一个问题,需要将科学计数法表示的数值的字符串转换成数值型数据参与算术运算, 然而,当使用int()方法进行转换时,直接报错了,如下: 然后在网上找转换方法,始终没有找到合适的方法,有的是自己写 ...

  7. 剑指Offer_编程题-003 - 输入一个链表,按链表值从尾到头的顺序返回一个ArrayList

    如题 (总结) 首节点也存放了值,所以ListNode t = listNode; 直接从头开始遍历即可. 简单题目,但是构建的时候出了点问题,毕竟需要自己简单测测. 掌握链表的构建方法, 还要根据题 ...

  8. Mysql insert on update

    数据库 Mysql INSERT INTO table (column_list) VALUES (value_list) ON DUPLICATE KEY UPDATE c1 = v1, c2 = ...

  9. pve配置

    U盘安装 推荐使用https://rufus.ie/ 刻录到U盘 (注意:以 DD 镜像 模式写入) 关闭订阅提醒 将if(data.status!=='Active')替换为if(false) se ...

  10. Flash完美跨域访问的方法

    先,你要确定以下几点,否则可能无法实现: 1.你要跨到哪个域,你必须能管理那域上文件,因为这里要放一个通行文件. 2.你的Flash如果只有SWF,那不一定能实现,因为有时,Flash的AS中,要加入 ...