CompletableFuture的底层实现

612 阅读6分钟

CompletableFuture 是 Java 8 扔给我们的并发“神器”,比 Future 能打多了。链式调用、异步回调,简直是程序员的“懒人福音”。对 JUC 老手来说,它是并发编程的“瑞士军刀”。但它底层咋实现的?别怕,今天我带你扒开它的“衣服”,看看里面藏着啥秘密。

1. 设计理念:从“收据”到“接力赛”

CompletableFuture 不是那种老实巴交的 Future,只给你张“任务收据”然后让你干等着。它更像个跑接力赛的选手:任务跑完一棒,自动把 baton(结果)递给下一棒,还能顺手处理异常。实现上,它靠这几招:

  • 任务接龙:支持链式调用(thenApplythenCompose 等),通过回调机制实现任务依赖。
  • 状态驱动:以结果状态为核心,动态触发后续操作。即结果出来就撒腿跑后续操作,不用你喊“开始”。
  • 线程池撑腰:默认靠 ForkJoinPool.commonPool(),省得你自己养线程。
  • 无锁并发:利用 CAS(Compare-And-Swap)操作,避免传统锁的开销。

比起 FutureTask 的“单打独斗”,CompletableFuture 是个“社交达人”,擅长拉帮结派干大事。它不像 FutureTask那样简单封装一个任务,而是设计了一个灵活的“任务接力系统”。


2. 数据结构:简简单单两件套

FutureTask 的单任务封装不同,CompletableFuture 更像一个“任务协调器”,其底层实现围绕状态管理回调栈展开。CompletableFuture 的核心字段非常精简,家底不多,就两个核心字段:

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    volatile Object result;       // 任务结果or异常,啥都能装
    volatile Completion stack;    // 回调栈,干活的“后援团”
    // ... 其他辅助字段和方法
}

2.1 result:状态与结果的统一载体:万能“口袋”

  • 啥都能装:没结果是 null,正常结果是个对象,异常就塞个 AltResult(内部类,专门抱 Throwable)。
  • 状态暗示:不用显式的 state 整数,result 自己会“说话”:
    • null:任务未完成。哥还没干完,别催。
    • AltResult:任务异常完成(AltResult 是一个内部类,包装 Throwable)。我翻车了,抱歉。
    • 其他非 null 值:任务正常完成,结果即该值。搞定,拿走吧!
  • 线程安全:靠 CAS(UNSAFE.compareAndSwapObject)更新,谁抢到谁说了算。

2.2 stack:回调任务的栈结构:即回调的“待办清单”

  • 是个栈:用 Completion 一个抽象类,表示后续操作的节点。给节点串起来,后进先出,像摞盘子。
  • 干啥用:存着依赖当前任务的“后援团”,比如 thenApply 的下一步操作。
  • 长啥样:单向链表,采用栈的形式(LIFO,后进先出)后面会细说。
  • 实现Completion 的子类(如 UniApplyBiConsumer)具体定义回调逻辑。

2.3 Completion:小弟们的“身份证”

abstract static class Completion extends ForkJoinTask<Void> implements Runnable, CompletableFuture.AsynchronousCompletionTask {
    Completion next; // 指向下一个回调节点
    abstract CompletableFuture<?> tryFire(int mode); // 执行回调逻辑
}
  • 抽象基类:每个节点是个 Completion,有 next 指针指向下家。
  • 分工明确:子类各司其职:
    • UniApply:管 thenApply,加工结果。
    • BiConsumer:管 thenCombine,合并任务。
    • UniExceptionally:救火队员,处理异常。
  • 每个 Completion 节点封装了回调函数和目标 CompletableFuture

这帮小弟就像等你下班的同事,任务一完,他们就冲上来喊:“轮到我了吧!”

3. 状态管理与完成机制:没有 state 的“隐形状态机”

不像 FutureTask 老老实实弄个 state 整数,CompletableFuture 玩得花哨,直接用 result 表示状态。咋完成的呢?

3.1 complete(T value):喊“搞定”

  • 流程
    • CAS 把 resultnull 改成 value,抢到了就成功。
    • 如果成功,调用 postComplete() 触发回调。(成功后喊一嗓子 postComplete(),唤醒后援团。)
  • 源码片段
    public boolean complete(T value) {
        boolean triggered = completeValue(value);
        if (triggered) postComplete();
        return triggered;
    }
    

就像外卖到了,你签个字,然后通知楼下等饭的同事。

3.2 completeExceptionally(Throwable ex):异常完成

  • 流程:将 result 设置为 new AltResult(ex),然后触发 postComplete()

任务翻车了,它还不忘安慰你:“别慌,我给你留了个错误日志!”

3.3 postComplete():回调触发

  • 干啥:在任务完成后,遍历 stack,执行所有依赖回调。
  • 源码片段
    final void postComplete() {
        Completion h;
        while ((h = stack) != null) {
            if (UNSAFE.compareAndSwapObject(this, STACK, h, null)) {
                while (h != null) {
                    h.tryFire(NESTED); // 点火!
                    h = h.next;
                }
            }
        }
    }
    
  • 细节:CAS 清栈,防止其他线程捣乱即并发修改,然后挨个执行 tryFire(),有点像放鞭炮,噼里啪啦。

4. 链式调用的实现:接力赛的“魔法”

CompletableFuture 的链式调用(如 thenApply)是其核心特性,底层依赖回调栈和动态任务创建。

4.1 thenApply 的流程

  • 方法签名
    public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)
    
  • 实现逻辑
    1. 创建一个新的 CompletableFuture<U>(结果容器)。
    2. 构造 UniApply 节点,封装 fn 和目标 CompletableFuture
    3. 检查当前 result
      • 已完成:立即执行 fn,设置新结果。(有结果:立刻加工,交给新 Future。)
      • 未完成:将 UniApply 推入 stack。(没结果:扔进 stack,等着。)
  • 源码片段
    public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {
        return uniApplyStage(null, fn);
    }
    
    private <U> CompletableFuture<U> uniApplyStage(Executor e, Function<? super T, ? extends U> f) {
        CompletableFuture<U> d = new CompletableFuture<U>();
        if (result != null) {
            uniApplyNow(result, d, f); // 立即执行
        } else {
            push(new UniApply<T,U>(e, d, this, f)); // 入栈等待
        }
        return d;
    }
    

4.2 异步版本:thenApplyAsync

  • 区别:将 UniApply 提交到线程池(默认 ForkJoinPool.commonPool()),通过 e.execute() 异步执行。即把 UniApply 扔给线程池跑,别挡主路。

5. 线程安全与并发控制

CompletableFuture 的线程安全完全依赖无锁设计:

  • CAS 操作

    • 使用 sun.misc.UnsafecompareAndSwapObject 更新 resultstack
    • 示例:
      static final sun.misc.Unsafe UNSAFE;
      static final long RESULT = UNSAFE.objectFieldOffset(CompletableFuture.class.getDeclaredField("result"));
      UNSAFE.compareAndSwapObject(this, RESULT, null, value);
      
  • 优点:避免锁竞争,适用于高并发场景。

  • 缺点:CAS 失败时可能重试,极端情况下性能下降。

  • 阻塞与唤醒

    • get() 使用 LockSupport.park() 阻塞调用线程。
    • postComplete()LockSupport.unpark() 唤醒等待线程。

6. 获取结果:get() 的实现

  • 逻辑
    • 检查 result,已完成则返回结果或抛异常。
    • 未完成则调用 await() 阻塞。
  • 源码片段
    public T get() throws InterruptedException, ExecutionException {
        Object r;
        if ((r = result) == null) {
            await(); // 阻塞等待
            r = result;
        }
        if (r instanceof AltResult) {
            throw new ExecutionException(((AltResult) r).ex);
        }
        return (T) r;
    }
    
    有了就返回,没就睡大觉。这货还挺有耐心,结果没出来就死等,跟排队买奶茶似的。

7. 线程安全:CAS 大法好

  • 无锁狂欢:全靠 sun.misc.Unsafe 的 CAS,更新 resultstack
    • 示例:UNSAFE.compareAndSwapObject(this, RESULT, null, value)
    • 优点:快如闪电,没锁的拖累。
    • 缺点:抢不过就得重试,运气不好可能多跑几圈。
  • 阻塞与唤醒
    • get()LockSupport.park() 睡一觉。
    • postComplete()unpark() 喊醒,像闹钟一样准时。

7. 性能优化与设计考量

  • 栈结构(LIFO):比队列更高效,减少内存分配。LIFO 省内存,操作快。
  • 延迟执行:回调尽量在结果可用时立即执行,减少线程切换。即能跑就跑,结果一出来就干活,少切换线程。
  • ForkJoinPool 集成:默认线程池支持工作窃取,即“偷活”,效率满分。适合短任务。
  • 内存可见性volatile 修饰 resultstack,确保多线程可见。

8. 与 FutureTask 的对比

特性FutureTaskCompletableFuture
任务支持单任务封装支持任务链和组合
状态管理显式 state 整数通过 result 间接管理
并发控制锁(同步块)无锁(CAS)
回调机制支持丰富回调(Completion

9. 总结

CompletableFuture 底层是个“小怪兽”:

  • resultstack 撑起状态和回调。
  • CAS 无锁玩得飞起。
  • Completion 链表串起接力赛。
  • ForkJoinPool 当苦力。

对 JUC 老手来说,它是并发编程的“瑞士军刀”。理解这些底层细节,下次写异步代码就能更得心应手——毕竟,知道它咋跑的,才能让它跑得更快!