CompletableFuture 超时功能有大坑!使用不当直接生产事故!

本文未经允许禁止转载!

上一篇文章《如何实现超时功能(以CompletableFuture为例)》中我们讨论了 CompletableFuture 超时功能的具体实现,从整体实现来说,JDK21前的版本有着内存泄露的bug,不过很少对实际生产有影响,因为任务的编排涉及的对象并不多,少量内存泄露最终会被回收掉。从单一功能内聚的角度来说,超时功能的实现是没有问题;然而由于并发编程的复杂性,可能会出现 Delayer 线程延迟执行的情况。本文将详细复现与讨论 CompletableFuture 超时功能的大坑,同时提供一些最佳实践指导。

2024年9月8日更新:CFFU 开源项目负责人李鼎(Jerry Lee) 更新了代码示例,点击这里查看

1. 问题复现

感谢 CFFU 开源项目负责人李鼎(Jerry Lee) 提供代码

public class CfDelayDysfunctionDemo {

  public static void main(String[] args) {
dysfunctionDemo();
System.out.println();
cffuOrTimeoutFixDysfunctionDemo();
} private static void dysfunctionDemo() {
logWithTimeAndThread("dysfunctionDemo begin");
final long tick = System.currentTimeMillis();
final List<CompletableFuture<?>> sequentCfs = new ArrayList<>(); CompletableFuture<Integer> incomplete = new CompletableFuture<>(); CompletableFuture<?> cf = incomplete.orTimeout(100, TimeUnit.MILLISECONDS)
.handle((v, ex) -> {
logWithTimeAndThread("[1] timout");
sleep(1000);
return null;
});
sequentCfs.add(cf); cf = incomplete.orTimeout(100, TimeUnit.MILLISECONDS)
.handle((v, ex) -> {
logWithTimeAndThread("[2] timout");
sleep(1000);
return null;
});
sequentCfs.add(cf); cf = incomplete.orTimeout(100, TimeUnit.MILLISECONDS)
.handle((v, ex) -> {
logWithTimeAndThread("[3] timout");
sleep(1000);
return null;
});
sequentCfs.add(cf); CompletableFuture.allOf(sequentCfs.toArray(CompletableFuture[]::new)).join();
logWithTimeAndThread("dysfunctionDemo end in " + (System.currentTimeMillis() - tick) + "ms");
} private static void cffuOrTimeoutFixDysfunctionDemo() {
logWithTimeAndThread("cffuOrTimeoutFixDysfunctionDemo begin");
final long tick = System.currentTimeMillis();
final List<CompletableFuture<?>> sequentCfs = new ArrayList<>(); CompletableFuture<Integer> incomplete = new CompletableFuture<>(); CompletableFuture<?> cf = CompletableFutureUtils.cffuOrTimeout(incomplete, 100, TimeUnit.MILLISECONDS)
.handle((v, ex) -> {
logWithTimeAndThread("[1] timout");
sleep(1000);
return null;
});
sequentCfs.add(cf); cf = CompletableFutureUtils.cffuOrTimeout(incomplete, 100, TimeUnit.MILLISECONDS)
.handle((v, ex) -> {
logWithTimeAndThread("[2] timout");
sleep(1000);
return null;
});
sequentCfs.add(cf); cf = CompletableFutureUtils.cffuOrTimeout(incomplete, 100, TimeUnit.MILLISECONDS)
.handle((v, ex) -> {
logWithTimeAndThread("[3] timout");
sleep(1000);
return null;
});
sequentCfs.add(cf); CompletableFuture.allOf(sequentCfs.toArray(CompletableFuture[]::new)).join();
logWithTimeAndThread("cffuOrTimeoutFixDysfunctionDemo end in " + (System.currentTimeMillis() - tick) + "ms");
} private static void logWithTimeAndThread(String msg) {
System.out.printf("%tF %<tT.%<tL [%s] %s%n",
System.currentTimeMillis(), Thread.currentThread().getName(), msg);
}
}

执行结果如下:

代码思路是这样的:有3个运行1秒的任务,在超时之后运行,不切线程池(都在 Delayer 线程运行),运行了3秒,不能在设置100ms的超时后运行,因为单线程排队了。handle 方法传入的回调函数在 Delayer 线程中执行了。

示例代码中解决超时线程延迟执行的方法是使用CFFU提供的安全 timeout 方法,本文后面会分析相关源码。

2. 问题分析

为什么handle方法里的回调会在 CompletableFutureDelayScheduler 中执行?

// 这里的代码逐步深入到调用栈内部
public <U> CompletableFuture<U> handle(
BiFunction<? super T, Throwable, ? extends U> fn) {
return uniHandleStage(null, fn);
} private <V> CompletableFuture<V> uniHandleStage(
Executor e, BiFunction<? super T, Throwable, ? extends V> f) {
if (f == null) throw new NullPointerException();
CompletableFuture<V> d = newIncompleteFuture();
Object r;
if ((r = result) == null)
// 加入回调栈中后续再执行
unipush(new UniHandle<T,V>(e, d, this, f));
else if (e == null)
// 有结果,直接执行
d.uniHandle(r, f, null);
else {
try {
e.execute(new UniHandle<T,V>(null, d, this, f));
} catch (Throwable ex) {
d.result = encodeThrowable(ex);
}
}
return d;
} final <S> boolean uniHandle(Object r,
BiFunction<? super S, Throwable, ? extends T> f,
UniHandle<S,T> c) {
S s; Throwable x;
if (result == null) {
try {
// 此次调用中 c 为空,无需关注UniHandle,甚至不需要知道UniHandle的具体职责
if (c != null && !c.claim())
return false;
if (r instanceof AltResult) {
x = ((AltResult)r).ex;
s = null;
} else {
x = null;
@SuppressWarnings("unchecked") S ss = (S) r;
s = ss;
}
// 执行回调
completeValue(f.apply(s, x));
} catch (Throwable ex) {
completeThrowable(ex);
}
}
return true;
}

我们把出现问题的原因简单总结一下:

CompletionStage 中不带 async 的方法可能会在不同的线程中执行。一般情况下,如果CF的结果已经计算出来,后续的回调在调用线程中执行,如果结果没有计算出来,后续的回调在上一步计算的线程中执行。

以下是一个简化的代码示例:

@Slf4j
public class TimeoutBugDemo {
public static void main(String[] args) {
new CompletableFuture<Integer>()
.orTimeout(1, TimeUnit.SECONDS)
.handle((v, ex) -> {
log.info("v: {}", v, ex);
return -1;
}).join();
}
}

handle 方法传入的回调方法会在delayer线程中执行,从执行日志看也确实如此:

Task :TimeoutBugDemo.main()
11:58:53.465 [CompletableFutureDelayScheduler] INFO com.example.demo.cftimeout.TimeoutBugDemo -- v: null
java.util.concurrent.TimeoutException: null
at java.base/java.util.concurrent.CompletableFuture$Timeout.run(CompletableFuture.java:2920)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)

3. CFFU 是如何解决线程传导的?

// CFFU 代码实现
public static <C extends CompletableFuture<?>> C cffuOrTimeout(
C cfThis, Executor executorWhenTimeout, long timeout, TimeUnit unit) {
requireNonNull(cfThis, "cfThis is null");
requireNonNull(executorWhenTimeout, "executorWhenTimeout is null");
requireNonNull(unit, "unit is null"); return hopExecutorIfAtCfDelayerThread(orTimeout(cfThis, timeout, unit), executorWhenTimeout);
} // 核心实现代码
private static <C extends CompletableFuture<?>> C hopExecutorIfAtCfDelayerThread(C cf, Executor executor) {
CompletableFuture<Object> ret = newIncompleteFuture(cf); // use `cf.handle` method(instead of `cf.whenComplete`) and return null in order to
// prevent reporting the handled exception argument of this `action` at subsequent `exceptionally`
cf.handle((v, ex) -> {
if (!atCfDelayerThread()) completeCf(ret, v, ex);
// 使用 executor 后,CF的后续回调操作就不会在Dalayer 线程中执行了
else executor.execute(() -> completeCf(ret, v, ex));
return null;
}).exceptionally(ex -> reportUncaughtException("handle of executor hop", ex)); return (C) ret;
} private static void completeCf(CompletableFuture<Object> cf, Object value, @Nullable Throwable ex) {
try {
// 写入到新CF中
if (ex == null) cf.complete(value);
else cf.completeExceptionally(ex);
} catch (Throwable t) {
if (ex != null) t.addSuppressed(ex);
reportUncaughtException("completeCf", t);
throw t; // rethrow exception, report to caller
}
}

基本思路将结果写入到新的 CompletableFuture 中,为了避免后续回调使用 Delayer 线程,改用新增的线程,保证线程传导的安全性。

提示:有时我们需要关注链式调用返回的是新值还是原有对象,比如 CompletableFuture#orTimeout 返回的是当前对象this, CFFU中返回的是新的 CompletableFuture。

4. 最佳实践的启示

  1. 使用优秀的 CompletableFuture 类库: CFFU,避免编程出错,减轻开发负担。
  2. 可参考我在《深入理解 Future, CompletableFuture, ListenableFuture,回调机制》一文中所讲的,如果使用CompletableFuture,应该尽量显示使用async*方法,同时显式传入执行器executor参数。
  3. 改为使用 Guava 中的 ListenableFuture。

CompletableFuture 超时功能有大坑!使用不当直接生产事故!的更多相关文章

  1. MySQL数据库连接重试功能和连接超时功能的DB连接Python实现

    def reConndb(self): # 数据库连接重试功能和连接超时功能的DB连接 _conn_status = True _max_retries_count = 10 # 设置最大重试次数 _ ...

  2. Redis源码解析:09redis数据库实现(键值对操作、键超时功能、键空间通知)

    本章对Redis服务器的数据库实现进行介绍,说明Redis数据库相关操作的实现,包括数据库中键值对的添加.删除.查看.更新等操作的实现:客户端切换数据库的实现:键超时相关功能的实现.键空间事件通知等. ...

  3. subprocess添加超时功能

    def TIMEOUT_COMMAND(command, timeout): """call shell-command and either return its ou ...

  4. Java 实现一个自己的显式锁Lock(有超时功能)

    Lock接口 package concurency.chapter9; import java.util.Collection; public interface Lock { static clas ...

  5. 一次 Redis 事务使用不当引发的生产事故

    这是悟空的第 170 篇原创文章 官网:http://www.passjava.cn 你好,我是悟空. 本文主要内容如下: 一.前言 最近项目的生产环境遇到一个奇怪的问题: 现象:每天早上客服人员在后 ...

  6. Java CompletableFuture 异步超时实现探索

    作者:京东科技 张天赐 前言 JDK 8 是一次重大的版本升级,新增了非常多的特性,其中之一便是 CompletableFuture.自此从 JDK 层面真正意义上的支持了基于事件的异步编程范式,弥补 ...

  7. 我向PostgreSQL社区贡献的功能:空闲会话超时

    经过约八个月的努力,终于完成了 PostgreSQL 空闲会话超时断开的功能. 该功能将在版本 14 中发布. 这是我第一次向 PostgreSQL 提供功能,虽然之前也有向社区提供过补丁,但是这次整 ...

  8. dubbo(九):timeout超时机制解析

    在网络请求时,总会有各种异常情况出现,我们需要提前处理这种情况.在完善的rpc组件dubbo中,自然是不会少了这一层东西的.我们只需要通过一些简单的配置就可以达到超时限制的作用了. dubbo的设计理 ...

  9. 安卓奇葩问题之:设置webView超时

    我只想说:what a fucking day! 今天要做一个webView的超时功能,于是开始百度,一看貌似很简单啊,于是开始copy了下面的代码. import java.util.Timer; ...

  10. Linux下connect超时处理【总结】

    1.前言 最近在写一个测试工具,要求快速的高效率的扫描出各个服务器开放了哪些端口.当时想了一下,ping只能检测ip,判断服务器的网络是连通的,而不能判断是否开放了端口.我们知道端口属于网络的应用层, ...

随机推荐

  1. Python prometheus_client使用方式

    #!/usr/bin/env python # -*- coding: utf8 -*- import socket,re from prometheus_client import generate ...

  2. (Redis基础教程之五)如何在Redis中操作字符串

    如何在ubuntu18.04上安装和保护redis 如何连接到Redis数据库 如何管理Redis数据库和Keys 如何在Redis中管理副本和客户端 如何在Redis中管理字符串 如何在Redis中 ...

  3. golang之命令行参数

    当我们希望通过命令行启动Golang程序,获取输入的各种形式参数时,该如何处理呢? [os.Args] os.Args是一个string的切片,用来存储所有的命令行参数,包括go run main.g ...

  4. word 文档签章控件生成的签章批量删除

    某个签章工具的word插件缺少批量插入签章的功能.同时,发现在投标工具中可以使用导出生成pdf时批量签章的功能.现在需要移除先前手动操作生成的多个签章,有如下发现-- 1.对少量签章,可以先选中签章右 ...

  5. 2020-2021 ACM-ICPC Brazil Subregional Programming Contest

    A. Sticker Album 你想要得到\(n\)张贴纸,每包礼物中等概率出现 \([A,B]\)范围内数量的贴纸,求需要买多少包礼物才能至少获得\(n\)张贴纸的期望次数 \(1 \leq n ...

  6. Codeforces Round 890 (Div. 2)

    Tales of a Sort 题解 找到最大的能够产生逆序对的数即可 暴力\(O(n^2)\)枚举即可 const int N = 2e5 + 10, M = 4e5 + 10; int n; in ...

  7. 金仓数据库数据迁移实战:从MySQL到KES的顺利迁移

    今天我们将开始实践金仓数据库的数据迁移功能.在此之前,我们一直使用的是简化版的 Docker 镜像,这个版本并没有集成可视化操作工具.因此,为了更方便地进行后续的操作,我们需要额外下载一个 Windo ...

  8. Tailwind CSS样式优先级控制

    前情 Tailwind CSS 是一个原子类 CSS 框架,它将基础的 CSS 全部拆分为原子级别,能达到最小化项目CSS.它的工作原理是扫描所有 HTML 文件.JavaScript 组件以及任何模 ...

  9. Ant Design Pro项目Popover位置微调

    前情 公司有经常需要做一些后台管理页面,我们选择了Ant Design Pro,它是基于 Ant Design 和 umi 的封装的一整套企业级中后台前端/设计解决方案. 产品效果图 最新接到的一个后 ...

  10. 无法加载文件 C:\Users\admin\AppData\Roaming\npm\anyproxy.ps1

    无法加载文件 C:\Users\admin\AppData\Roaming\npm\anyproxy.ps1 我们右击开始菜单,以管理员身份打开power shell 运行命令: set-Execut ...