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. 【昌哥IT课堂】MySQL8.3 EXPLAIN中的新JSON格式(译)

    MySQL提供了两个用于分析查询计划的强大工具:EXPLAIN和EXPLAIN ANALYZE.EXPLAIN显示优化器选择的执行计划,并在执行之前停止,而EXPLAIN ANALYZE实际执行查询并 ...

  2. JDocumentEditor

    package infonode; /** * * @author sony */ //JDocumentEditor.java import java.awt.*; import java.awt. ...

  3. C#/.NET/.NET Core技术前沿周刊 | 第 15 期(2024年11.25-11.30)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  4. 2024 盘古石数据取证 服务器部分wp

    1. 分析内部IM服务器检材,在搭建的内部即时通讯平台中,客户端与服务器的通讯端口是:[答案格式:8888][★☆☆☆☆] 8065 2. 分析内部IM服务器检材,该内部IM平台使用的数据库版本是: ...

  5. 关于IMultiValueConverter的使用

    在前端向后端传递数据的过程中,因为涉及多个属性的调用,将数据绑定到CommandParameter,采用了多值转换器进行数据传递. class MultiBindingConverter : IMul ...

  6. zz 失血模型与充血模型等

    失血模型与充血模型 | 三秋 (贫血模型)优点是系统的层次结构清楚,各层之间单向依赖,Client->(BusinessFacade)->BusinessLogic->Data Ac ...

  7. Xshell无法连接22端口问题解决办法汇总

    Xshell软件在进行远程连接过程中,会出现端口连接报错的问题,提示:"该IP地址的22端口连接失败",这是怎么回事?今天小编就xshell软件无法连接22端口的问题,整理相关情形 ...

  8. 谈谈 HTTP/2 的协议协商机制

    在过去的几个月里,我写了很多有关 HTTP/2 的文章,也做过好几场相关分享.我在向大家介绍 HTTP/2 的过程中,有一些问题经常会被问到.例如要部署 HTTP/2 一定要先升级到 HTTPS 么? ...

  9. 龙哥量化:TB交易开拓者_趋势跟踪策略_多策略对单品种_A00011880206期货量化策略,严格的用样本内参数, 跑样本外数据,滚动测试未来行情

    如果您需要代写技术指标公式, 请联系我. 龙哥QQ:591438821 龙哥微信:Long622889 也可以把您的通达信,文华技术指标改成TB交易开拓者的自动交易量化策略. 量化策略介绍 投资标的: ...

  10. 关于Qt程序中动态和静态的几点总结

    在Qt程序中,分动态库版本的Qt和静态库版本的Qt. 官方默认提供的二进制包就是动态库版本的Qt,如果自行编译则编译的时候对应参数 -shared. 静态库版本的Qt需要自行编译,编译的时候对应参数 ...