写 Java 并发的时候我最开始对 FutureTask 的认知停留在把 Callable 丢进去然后 get() 拿返回值这个层面。直到有一天我想自己实现一个简单的异步任务才发现 Runnable 的 run() 没有返回值、Callable 能返回但没法直接丢给 Thread两个接口之间横着一条天然的鸿沟。FutureTask 最巧妙的地方就是它同时实现了 Runnable 和 Future 两套身份对线程池来说它是一个普普通通的 Runnable对调用方来说它又是一个可以查询任务状态、阻塞获取结果、取消任务的 Future 凭证。这篇文章我从源码层面拆一下 FutureTask 的适配思路、状态机设计、阻塞唤醒机制和完整执行链路适合那些想深入 JUC 底层、或者已经在用 FutureTask 但始终没搞懂结果到底怎么回来的这类问题的开发者。1. 一个既是任务、又是凭证的类适配的入口设计1.1 Runnable 和 Callable 之间的那条鸿沟先看两个接口本身。Runnable 的定义极简public interface Runnable { void run(); }。run() 没有返回值也不能抛出受检异常它是为线程要执行的一段代码而生的。Callable 则是public interface CallableV { V call() throws Exception; }call() 可以返回一个泛型结果也可以把异常往上抛。这两个接口简直是对着设计的一个能当任务但不能给结果一个能给结果但没法直接交给 Thread 或者 ThreadPoolExecutor 去执行。那早期没有 FutureTask 的时候怎么办我见过不少土办法定义一个共享容器任务线程把结果写进去主线程轮询标志位或者自己维护一个MapRunnable, Object任务执行完往里塞结果主线程按 Runnable 查。这些方案都能跑但线程安全、内存可见性、等待通知全都得自己手搓稍不留神就是并发 Bug。1.2 RunnableFuture把两个身份焊在一起FutureTask 的核心设计可以从它的继承关系一眼看穿public class FutureTaskV implements RunnableFutureV public interface RunnableFutureV extends Runnable, FutureV { void run(); }RunnableFuture 这个接口非常直白你既是 Runnable可以被提交给任何只认 Runnable 的执行器你又是 Future可以调用 get()、cancel()、isDone() 来管理任务生命周期。FutureTask 就是这个接口的默认实现。这种一个对象承担两种职责的设计本质上就是适配器模式的一种落地。调用方视角下提交任务时只需要new FutureTask(callable)然后把它交给线程池或者new Thread(task).start()完全不关心内部适配细节另一个视角下同一个对象保存了任务执行结果后续随时可以通过 get() 把这个结果取出来。1.3 为什么适配不入线程池内部有人可能会问线程池的 execute() 方法只接受 Runnable那为什么不让 ThreadPoolExecutor 内部把 Callable 包一下、再单独维护一个任务到 Future 的映射就完事了呢这个方案理论上可行但代价实在太大。如果在线程池内部做适配就得为每一个提交的 Callable 额外创建包装对象还要用一个并发 Map 保存 Runnable 和 Future 的对应关系。任务完成前 Future 不能丢任务完成后又要清理映射这个映射表的生命周期和线程池绑定在一起内存回收、并发访问全是麻烦。FutureTask 把任务本身和任务凭证合二为一任务对象自己保管结果线程池最多只把 callable 字段置空帮助 GC完全不需要外部映射。这也是 Doug Lea 那一套并发设计里很典型的思路能用对象自身状态解决的问题就不引入额外结构。2. 状态机跨线程安全的最大底座2.1 七个状态分别代表什么FutureTask 内部有一个private volatile int state;字段整个类的状态流转全靠它。状态常量定义如下private static final int NEW 0; private static final int COMPLETING 1; private static final int NORMAL 2; private static final int EXCEPTIONAL 3; private static final int CANCELLED 4; private static final int INTERRUPTING 5; private static final int INTERRUPTED 6;下面是每个状态的含义和最终流向状态数值含义是否终态NEW0任务新建尚未被执行否COMPLETING1正在设置结果outcome 即将写入否NORMAL2正常执行完成是EXCEPTIONAL3执行过程抛出异常是CANCELLED4被取消未中断运行线程是INTERRUPTING5调用 cancel(true)正在中断线程否INTERRUPTED6中断完成是这里有一个看起来很不起眼、实则很关键的点state 只能从 NEW 开始单向推进最终停在 NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED 这四个终态之一。任务一旦完成或者被取消就永远不可能回到 NEW所以 FutureTask 不能复跑。这个设计直接决定了整个类的行为run() 只有在 state 还是 NEW 的时候才真正执行任务其余任何状态下调用 run() 都是直接返回。2.2 为什么强依赖 volatile 和 CASstate 是 volatile 的保证了所有线程读取到这个字段时都能拿到最新写入值状态的变更大量依赖 CAS保证多个线程同时尝试推进状态时只有一个能成功。为什么要这么较真因为 FutureTask 的工作线程、等待结果的线程、发出 cancel 的线程很可能是三个不同的线程它们之间没有任何锁所有的协同都落在 state 之上。举一个最简单的例子任务执行完毕工作线程把 state 从 COMPLETING 推进到 NORMAL主线程在 get() 里循环读 state一旦读到 NORMAL 就认为结果就绪了。如果没有 volatile主线程可能一直读到旧值如果没有 CAS两个线程同时调用 set() 可能会把同一个任务设置两次结果状态就乱了。2.3 outcome 的写入顺序为什么先写结果、后置终态FutureTask 里保存结果的字段是private Object outcome;它本身并不是 volatile 的。你可能会疑惑outcome 不是 volatile那等待线程怎么保证能看到最新结果答案藏在写入顺序里。在 set() 方法中源码大致是这样处理的protected void set(V v) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome v; UNSAFE.putOrderedInt(this, stateOffset, NORMAL); finishCompletion(); } }也就是先把 state 从 NEW CAS 到 COMPLETING然后写入 outcome最后再写 state 到 NORMAL。这是一个典型的发布安全模式普通写操作 outcomev 后面紧跟一个有序写操作 stateNORMAL读到 stateNORMAL 的线程由于设置了 happens-before 关系再回头读 outcome 一定能看到最新值。这就是 COMPLETING 中间态的意义——它创造了一个结果正在落盘的窗口等待线程看到 COMPLETING 时不会把半成品当结果拿走而是乖乖继续等待。3. get() 阻塞的秘密等待链表与 LockSupport 的配合3.1 get() 的快速通道get() 方法源码很短public V get() throws InterruptedException, ExecutionException { int s state; if (s COMPLETING) s awaitDone(false, 0L); return report(s); }首先读一次 state如果任务已经完成state COMPLETING直接进入 report() 把 outcome 取出来如果任务还没完成或者正在完成就进入 awaitDone() 阻塞等待。report() 的逻辑也很清晰NORMAL 就直接强转返回值CANCELLED 或更高状态抛 CancellationExceptionEXCEPTIONAL 就包一层 ExecutionException 抛给调用方。3.2 awaitDone 里的等待链表awaitDone() 是整个 get() 的精髓。FutureTask 内部维护着一个volatile WaitNode waiters的无锁链表每个调用 get() 而被阻塞的线程都会被包装成一个 WaitNode 节点通过 CAS 头插法挂到链上。核心循环逻辑可以简化成下面几步读 state如果大于 COMPLETING说明任务已经完成把自己从链表里摘掉返回状态如果 state 等于 COMPLETING说明结果正在写入当前线程执行Thread.yield()让出一下 CPU等下轮循环再读如果当前线程被中断清理链表中自己的节点抛出 InterruptedException如果节点还没创建创建一个 WaitNode如果节点还没入队用 CAS 把节点头插到 waiters 链表最后调用LockSupport.park(this)或者带超时的LockSupport.parkNanos(this, nanos)真正阻塞。这个链表的目的是什么因为调用 get() 等待同一个任务的线程可能非常多如果用一个锁加条件队列每次唤醒所有线程都免不了锁竞争。用无锁链表加 LockSupport 的好处是等待线程各 sleep 各的任务完成时统一 unpark 就好不需要持有任何锁。3.3 完成后的唤醒finishCompletion任务完成时set() 或 setException() 末尾会调用 finishCompletion()。它的逻辑是先通过 CAS 把 waiters 置为 null拿到旧链表然后遍历链表中每个 WaitNode把节点里的 thread 取出来执行LockSupport.unpark(t)。这样所有正在 get() 里 park 的线程都会被唤醒重新进入循环读取 state发现已经完成就退出等待返回结果。这里必须说一下 LockSupport 相比 Object.wait/notify 的巨大优势。wait/notify 必须在 synchronized 块内使用而且 notify 如果早于 wait 发生线程可能永远睡死过去。LockSupport 的 unpark 可以先于 park 调用每个线程有一个许可permit的概念预先 unpark 一次线程后续 park 时不会真的阻塞直接消费掉这个许可继续执行。因此 FutureTask 的唤醒机制天然不存在过早通知导致丢失信号的问题。3.4 超时版 get 的实现思路带超时版本的get(long timeout, TimeUnit unit)走的是同一个 awaitDone只是 timed 参数为 true。在进入等待之前它会计算一个 deadlineSystem.nanoTime() unit.toNanos(timeout)。每一次循环都重新计算剩余时间nanos deadline - System.nanoTime()如果剩余时间已经小于等于 0就清理掉自己的 WaitNode返回当前 state。外层看到 state 仍然 COMPLETING就抛 TimeoutException如果恰好任务刚刚完成则不抛超时直接返回结果。所以超时语义是到点还没完成就放弃等待并不是严格到纳秒级别剩余一点点时间时任务完成也会正常返回。这里还有一个细节值得注意awaitDone 中会先把 COMPLETING 状态的线程执行 yield 再重读为什么不直接 park因为 COMPLETING 是一个极短的过渡态紧接着状态就会变成 NORMAL 或 EXCEPTIONAL线程让出 CPU 转一圈再读大概率就已经完成省掉一次 park/unpark 的开销。这种对短暂中间态做忙等待的处理方式其实也值得在业务代码里借鉴。4. run() 方法里的每一步从 call() 到结果落袋4.1 第一道防线runner 的 CAS 抢占run() 方法的开头是 FutureTask 整个并发逻辑里我最喜欢的一段public void run() { if (state ! NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; // ... }这里做了两件事先检查 state 是否为 NEW不是就返回然后用 CAS 把 runner 字段从 null 替换成当前线程。runner 字段是 volatile 的这个 CAS 保证了同一个 FutureTask 即使被多个线程同时执行 run()也只有一个线程能够抢占成功真正去调用内部的 Callable其余线程直接返回。你可能会想为什么要多此一举用 runner 来抢占而不是只看 state因为从状态还是 NEW到真正开始调 call()之间有一个时间窗口。如果两个线程同时发现 state 是 NEW 都想执行单靠状态判断不够runner CAS 相当于给谁是这个任务的执行者做了唯一标记。这一手非常巧妙地避免了重复执行而且对线程池的场景也很友好因为同一个任务可能被同一个 Worker 多次 getTask 取到或者被多个 Worker 抢到但真正执行的只有一次。4.2 call() 的结果与异常分别怎么存成功执行时走 set()前面已经讲过先 CAS 到 COMPLETING写入 outcome再推进到 NORMAL。异常执行时走 setException()protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome t; UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); finishCompletion(); } }异常对象本身也会被当作结果存进 outcome只是状态置为 EXCEPTIONAL。这样设计非常克制执行线程的任务就是执行 Callable至于结果如何转交给调用方是 FutureTask 的事异常也一样被包装成一个结果等工作线程把状态推进完成后调用方在 get() 里才把它转换成 ExecutionException 抛出来。为什么要包一层 ExecutionException 而不是直接把原始异常抛给 get()因为 get() 是通用接口无法预知调用方具体想捕获什么类型的异常统一包成 ExecutionException调用方只需要catch (ExecutionException e)再通过e.getCause()拿原始异常整个 API 的使用就非常规整。另外这种设计还避免了受检异常在跨线程传递时被方法签名限制的问题。4.3 finally 里的清理和中断协作run() 的 finally 块里有两段逻辑finally { runner null; int s state; if (s INTERRUPTING) handlePossibleCancellationInterrupt(s); }runner 置 null 是把当前执行者标记清掉方便后续 cancel(true) 等逻辑判断。但这里要注意任务完成之后 state 已经是终态runner 置 null 并不会让任务可以再次执行。handlePossibleCancellationInterrupt 是 JUC 源码里很少见的主动让出等待private void handlePossibleCancellationInterrupt(int s) { if (s INTERRUPTING) { while (state INTERRUPTING) Thread.yield(); } }如果 run() 结束前发现状态是 INTERRUPTING说明有另一个线程正在执行 cancel(true) 并准备 interrupt 当前任务线程。这里必须一直 yield 等到 cancel 线程把状态推进到 INTERRUPTED 再退出否则工作线程可能已经离开 run()而 cancel 线程还在尝试 interrupt导致中断信号丢失。这种处理方式看着笨拙但恰恰是为了保证 cancel 的语义完整。4.4 顺便说说 runAndResetFutureTask 还留了一个受保护的 runAndReset() 方法它和 run() 很像区别是执行完 Callable 后不保存结果、不推进状态到终态而是保持 NEW供 ScheduledThreadPoolExecutor 这类需要周期性执行的任务使用。这个方法平时几乎用不到但理解了它你会更清楚 FutureTask 的状态机设计是多么克制连作为周期任务基石的方法也没破坏任务只能被正确推进一次的核心原则。5. 从 submit 到 get 的完整链路线程池视角5.1 submit 里发生的其实只有两件事平时我们写ExecutorService pool Executors.newFixedThreadPool(4); FutureString f pool.submit(() - hello);的时候AbstractExecutorService 的 submit(Callable) 内部逻辑非常简单public T FutureT submit(CallableT task) { if (task null) throw new NullPointerException(); RunnableFutureT ftask newTaskFor(task); execute(ftask); return ftask; }newTaskFor 默认就是new FutureTask(callable)所以返回的 Future 真实类型就是 FutureTask。这里还有一个模板方法设计newTaskFor 是 protected 的子类可以通过重写它返回自定义的 RunnableFuture很多高性能框架就是这么扩展 FutureTask 的。再往下的 execute(ftask) 就把 FutureTask 当成一个普通 Runnable 交给了线程池。ThreadPoolExecutor 内部完全不知道这个 Runnable 内部藏着一个 Callable也不需要知道——它只负责在某个 Worker 线程里执行 task.run()。5.2 Worker 线程里的旅程把时序捋一下整个执行链路是这样的主线程调用 submit构造出一个 FutureTask通过 execute() 把它作为任务对象加入线程池线程池内部某个 Worker 线程通过 getTask() 拿到这个 taskWorker 线程执行 task.run()也就是 FutureTask 的 run()run() 里通过 runner 的 CAS 抢占到执行权后调用内部包装的 Callable.call()Callable 正常返回结果写入 outcome状态推进到 NORMALfinishCompletion() 唤醒所有等待者主线程在 get() 的 awaitDone() 中被 unpark 唤醒重新读 state 发现已经完成调用 report() 把 outcome 强转成 V 返回。整个过程里线程池只扮演了调度器的角色任务状态管理、结果保存、等待唤醒这些核心逻辑全部由 FutureTask 自己完成。这也正是我能放心把它当作标准并发原语使用的原因——边界划分非常清晰。5.3 cancel 与 get 超时的组合拳实际业务里最常见的用法是配合超时FutureString f pool.submit(callableTask); try { String result f.get(3, TimeUnit.SECONDS); } catch (TimeoutException e) { f.cancel(true); }cancel(true) 的执行流程值得特别注意。它首先判断 state 是否为 NEW再用 CAS 把 state 置为 INTERRUPTING然后取出 runner 字段对应的线程执行 interrupt()最后把状态推进到 INTERRUPTED。也就是说cancel(true) 并不保证任务一定停下来——如果任务内部不响应中断信号它照样会跑完。但从 FutureTask 的角度看它的语义是完整的取消成功后所有在 get() 里等待的线程会被唤醒并且 get() 会抛出 CancellationException。如果任务已经完成再调用 cancelCAS 会失败返回 false。这个细节在业务里挺实用可以用cancel(false)去试探取消一个可能完成了的任务返回 false 就说明任务已经结束不用再担心后台资源泄漏。5.4 不用线程池FutureTask 也能直接用有些轻量场景根本不需要引入线程池FutureTask 配合 Thread 就能工作FutureTaskInteger task new FutureTask(() - { Thread.sleep(1000); return 42; }); new Thread(task).start(); Integer result task.get();Thread 构造器只认 RunnableFutureTask 恰好就是 Runnable。这种写法的价值在于它帮你把异步执行和等待结果两件事统一到了一个对象上代码可读性远好于自己搞一个共享变量再轮询。6. 实战建议与几个容易踩的坑6.1 用 FutureTask 子类重写 done() 做完成回调FutureTask 有一个钩子方法 done()默认实现是空的它会在任务完成、所有等待线程被唤醒之后被调用调用者是完成任务的那个 Worker 线程。利用它可以做一些轻量的完成回调比如打日志、发信号、清理资源FutureTaskString task new FutureTask(() - { Thread.sleep(2000); return done; }) { Override protected void done() { // 这里不需要担心锁任务已完成 System.out.println(run 已完成等待线程正在被唤醒); } };要注意的是done() 里调用 get() 时如果任务是异常完成的会抛出 ExecutionException所以最好用 try/catch 包一层。我第一次用这个钩子的时候就忽略了这一点异常任务在回调里还炸了一次。6.2 几个必须避开的坑不要在 Callable 内部调用同一个 FutureTask 的 get()。这是一个典型的自等死锁执行线程在执行 call()却跑去 get() 等待自己完成而能完成任务的线程就是它自己结果谁也无法推进状态线程永久阻塞。正确做法是只由外部线程调用 get()。不要以为同一个 FutureTask 提交两次会被执行两次。即使在两个线程里执行同一个 task.run()runner 的 CAS 也只能让一个线程成功另一个直接返回。你需要的是每次新 new 一个 FutureTask。使用 get() 无限等待时要格外小心。如果任务因为某种原因始终不结束所有调用 get() 的线程都会一直 park这在线程池场景下可能会把池子里的线程全部占满。一个现实的做法是尽量用带超时的 get(long, TimeUnit)并在超时后评估是否需要 cancel。6.3 和 CompletableFuture 的边界被问得比较多的一个问题是有了 FutureTask 还要 CompletableFuture 干什么我的理解是FutureTask 更接近单个异步任务的结果获取原语它的模型是一个任务一个 Future一次等待CompletableFuture 则是异步编排框架善于把多个异步操作串成流水线还有 thenApply、thenCombine、exceptionally 这些组合操作。如果你只是想让某个后台线程执行一段耗时逻辑并回传结果FutureTask 足够了简单直接语义清晰。如果要做多任务并行编排、回调链、异常恢复那就直接上 CompletableFuture没必要拿 FutureTask 硬凑。两者不是替代关系而是不同抽象层级的产物。6.4 读源码时抓的三根主线最后给想读 FutureTask 源码的朋友一个建议不要一头扎进 CAS 偏移量的细节里抓住三根主线就能把整份源码吃透。第一根线是状态机state 的七种状态如何单向流转哪些是终态哪些是过渡态。第二根线是发布安全outcome 普通字段为什么能安全跨线程读取关键是先写结果、再置终态、等待者读终态后再读结果的顺序保证。第三根线是无锁等待WaitNode 链表加 LockSupport.park/unpark 替代了传统的 synchronized 加 wait/notify解除了锁依赖也消除了唤醒信号丢失的问题。这三根线搞明白之后再看 run()、get()、cancel() 这些方法会发现它们都只是在状态机上做文章而已。最后说点个人体会我第一次追 FutureTask 源码时最惊讶的是整个类几乎没有用到 synchronized 关键字复杂的状态流转全靠 volatile 加 CAS 加 LockSupport 撑起来。后来我写业务代码也养成了一个习惯遇到两个线程要协同一个等结果一个出结果的场景第一反应不是加锁而是先画一个最小状态机把谁负责推进状态、谁负责消费状态理清楚。这种并发思维比死记几个 API 有用得多至少能让你面对各种并发问题时知道往哪个方向去拆解。希望这篇剖析对你也有同样的启发。