Implementing Asynchronous Timeout with CompletableFuture in Java (JDK 8 & JDK 9)

This article explains how to use Java's CompletableFuture for parallel execution, demonstrates the limitations of simple timeout handling in JDK 8, and shows how JDK 9's built‑in orTimeout method and a custom utility class can provide precise asynchronous timeout control for backend services.

JD Tech
JD Tech
JD Tech
Implementing Asynchronous Timeout with CompletableFuture in Java (JDK 8 & JDK 9)

JDK 8 introduced CompletableFuture, enabling event‑driven asynchronous programming and allowing parallel execution of tasks such as multiple RPC calls. A simple serial example shows two compute calls executed one after another, taking at least four seconds.

By creating asynchronous tasks with CompletableFuture.supplyAsync and aggregating them with thenCombineAsync, the same work can be completed in roughly two seconds, as demonstrated by the parallel demo code.

public static void main(String[] args) {
    // 仅简单举例,在生产代码中不推荐这样编写
    // 统计耗时的函数
    time(() -> {
        CompletableFuture<Integer> result = Stream.of(1, 2)
            // 创建异步任务
            .map(x -> CompletableFuture.supplyAsync(() -> compute(x), executor))
            // 聚合
            .reduce(CompletableFuture.completedFuture(0), (x, y) -> x.thenCombineAsync(y, Integer::sum, executor));
        // 等待结果
        try {
            System.out.println("结果:" + result.get());
        } catch (ExecutionException | InterruptedException e) {
            System.err.println("任务执行异常");
        }
    });
}
// 输出示例
// [async-1]: 任务执行开始:1
// [async-2]: 任务执行开始:2
// [async-1]: 任务执行完成:1
// [async-2]: 任务执行完成:2
// 结果:3
// 耗时:2 秒

When tasks have unpredictable latency, a timeout is needed. Using Future.get(long, TimeUnit) allows each CompletableFuture to be bounded, but handling timeouts manually can be verbose, as shown in the following example.

public static void main(String[] args) {
    // 仅简单举例,在生产代码中不推荐这样编写
    time(() -> {
        List<CompletableFuture<Integer>> result = Stream.of(1, 2)
            .map(x -> CompletableFuture.supplyAsync(() -> compute(x), executor))
            .toList();
        int res = 0;
        for (CompletableFuture<Integer> future : result) {
            try {
                res += future.get(2, SECONDS);
            } catch (ExecutionException | InterruptedException | TimeoutException e) {
                System.err.println("任务执行异常或超时");
            }
        }
        System.out.println("结果:" + res);
    });
}
// 输出示例
// [async-2]: 任务执行开始:2
// [async-1]: 任务执行开始:1
// [async-1]: 任务执行完成:1
// 任务执行异常或超时
// 结果:1
// 耗时:2 秒

JDK 9 added native timeout support with orTimeout and completeOnTimeout. The implementation creates a scheduled task that throws a TimeoutException if the original future is not completed in time, and cancels the timer when the future finishes.

public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit) {
    if (unit == null) throw new NullPointerException();
    if (result == null)
        whenComplete(new Canceller(Delayer.delay(new Timeout(this), timeout, unit)));
    return this;
}

static final class Timeout implements Runnable {
    final CompletableFuture<?> f;
    Timeout(CompletableFuture<?> f) { this.f = f; }
    public void run() {
        if (f != null && !f.isDone())
            f.completeExceptionally(new TimeoutException());
    }
}

static final class Delayer {
    static ScheduledFuture<?> delay(Runnable command, long delay, TimeUnit unit) {
        return delayer.schedule(command, delay, unit);
    }
    static final class DaemonThreadFactory implements ThreadFactory {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("CompletableFutureDelayScheduler");
            return t;
        }
    }
    static final ScheduledThreadPoolExecutor delayer;
    static {
        delayer = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory());
        delayer.setRemoveOnCancelPolicy(true);
    }
}

static final class Canceller implements BiConsumer<Object, Throwable> {
    final Future<?> f;
    Canceller(Future<?> f) { this.f = f; }
    public void accept(Object ignore, Throwable ex) {
        if (ex == null && f != null && !f.isDone())
            f.cancel(false);
    }
}

For projects still on JDK 8, a compatible utility can be built using the same principles. The provided CompletableFutureExpandUtils class offers an orTimeout method that schedules a timeout task and cancels it when the future completes, mimicking JDK 9 behavior.

package com.jd.jr.market.reduction.util;

import com.jdpay.market.common.exception.UncheckedException;
import java.util.concurrent.*;
import java.util.function.BiConsumer;

/**
 * CompletableFuture 扩展工具
 */
public class CompletableFutureExpandUtils {
    public static <T> CompletableFuture<T> orTimeout(CompletableFuture<T> future, long timeout, TimeUnit unit) {
        if (unit == null) throw new UncheckedException("时间的给定粒度不能为空");
        if (future == null) throw new UncheckedException("异步任务不能为空");
        if (future.isDone()) return future;
        return future.whenComplete(new Canceller(Delayer.delay(new Timeout(future), timeout, unit)));
    }
    static final class Timeout implements Runnable {
        final CompletableFuture<?> future;
        Timeout(CompletableFuture<?> future) { this.future = future; }
        public void run() {
            if (future != null && !future.isDone())
                future.completeExceptionally(new TimeoutException());
        }
    }
    static final class Canceller implements BiConsumer<Object, Throwable> {
        final Future<?> future;
        Canceller(Future<?> future) { this.future = future; }
        public void accept(Object ignore, Throwable ex) {
            if (ex == null && future != null && !future.isDone())
                future.cancel(false);
        }
    }
    static final class Delayer {
        static ScheduledFuture<?> delay(Runnable command, long delay, TimeUnit unit) {
            return delayer.schedule(command, delay, unit);
        }
        static final class DaemonThreadFactory implements ThreadFactory {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                t.setName("CompletableFutureExpandUtilsDelayScheduler");
                return t;
            }
        }
        static final ScheduledThreadPoolExecutor delayer;
        static {
            delayer = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory());
            delayer.setRemoveOnCancelPolicy(true);
        }
    }
}

In summary, while JDK 8 requires custom code to achieve reliable asynchronous timeout, JDK 9 and later provide built‑in methods that simplify the implementation and improve precision, making CompletableFuture a robust tool for backend concurrency control.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyCompletableFutureJDK8Async TimeoutJDK9
JD Tech
Written by

JD Tech

Official JD technology sharing platform. All the cutting‑edge JD tech, innovative insights, and open‑source solutions you’re looking for, all in one place.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.