FutureTask原理剖析:Callable适配Runnable与结果获取
FutureTask 原理剖析如何将 Callable 适配为 Runnable 并获取结果在 Java 并发编程里FutureTask是我见过最耐看的一个类。它本身不是用来干活的而是用来“接活儿”的一边接住Callable这种能返回计算结果的任务一边把它包装成Runnable塞给 Thread 或线程池去执行最后再让你通过 get() 把结果取回来。这个类我翻过好几遍源码每次都有新收获尤其它内部那套状态机和线程等待唤醒机制设计得相当精巧。这篇博客我就拿实际代码和源码片段把 FutureTask 的核心原理掰开揉碎讲清楚重点是它到底怎么把 Callable 适配成 Runnable以及 get() 背后的阻塞与唤醒逻辑。不搞虚的适合所有写 Java 并发代码、尤其是用线程池时想优雅拿返回值的同学。相信不少朋友刚接触并发时都有过这样的困惑Thread 只认 Runnable可 Runnable 的 run() 方法偏偏没有返回值业务里又有太多场景需要拿到一个异步计算的结果。早期要不就得自己写回调要不就得搞一个共享变量加等待通知机制麻烦且容易出错。FutureTask 的意义就在于它把这两件事封装成了标准组件你只要写好 Callable剩下“异步执行、结果存取、超时控制、取消任务”这些脏活累活它全包了。1. 为什么需要 FutureTask从 Runnable 的短板说起1.1 Runnable 与 Callable 的根本差异先看最基础的接口定义。Runnable 的 run() 长得特别朴素没有返回值也不能抛受检异常。Callable 的 call() 则有一个泛型返回值throws Exception 直接声明了可以抛出异常。FunctionalInterface public interface Runnable { public abstract void run(); } FunctionalInterface public interface CallableV { V call() throws Exception; }就这俩接口的差异决定了它们在并发体系里各自的定位。Runnable 适合“只关心过程不关心结果”的任务比如打日志、刷缓存、发通知Callable 适合“必须拿到结果”的任务比如远程接口调用、复杂计算、批量查询。Thread 类的构造函数只接受 Runnable线程池的 execute() 方法也只接受 Runnable而 submit() 才能收 Callable。为什么因为底层要拿到结果并封装成 Future就是 FutureTask 在做的事。这里有个容易忽略的细节Runnable 的 run() 是“不能抛受检异常”的但 Callable 的 call() 是“可以抛任何异常”的。所以当 FutureTask 把 Callable 适配为 Runnable 之后run() 内部必须捕获 call() 抛出的异常不能让它直接冒出去。否则就会破坏 Runnable 对异常处理的基本约定线程池也会不知所措。捕获之后怎么办存起来等 get() 的时候再以 ExecutionException 的形式吐出来。这是整个适配逻辑里最关键的一个点。1.2 适配器模式在 FutureTask 中的体现FutureTask 干的事本质上就是适配器模式。它把一个 Callable 适配成了 Runnable类声明就写得很直白FutureTask implements RunnableFutureRunnableFuture 又同时继承了 Runnable 和 Future。所以它既是 Runnable 又是 Future既能交给线程执行又能让调用方查询和获取结果。public class FutureTaskV implements RunnableFutureV { } public interface RunnableFutureV extends Runnable, FutureV { void run(); }从设计模式角度看FutureTask 这里用的不是简单的对象适配器而是“接口合并”的实现方式。它同时拥有两种身份作为 Runnable它能被 Thread 直接接收作为 Future它提供了 get()、cancel()、isDone()、isCancelled() 这些方法。调用方视角根本不用关心它内部怎么适配的只需要持有 FutureTask 引用提交给执行器然后 get() 就完了。还记得代码里有个不引人注意的内部类 RunnableAdapter这才是最原始的适配工具。早期 JDK 里 Executors.callable(Runnable) 会把 Runnable 包成 CallableFutureTask 构造器也直接支持传入 Runnable 加固定结果。后来 JDK 8 的 FutureTask 调整了实现方式直接在构造器里把 Callable 包成 RunnableAdapter内部通过一个 callable 字段持有任务本体。而你传入 Runnable 时会被包装成Executors.callable(runnable, result)本质上还是 Callable。// Executors 中的适配实现JDK 8 public static T CallableT callable(Runnable task, T result) { if (task null) throw new NullPointerException(); return () - { task.run(); return result; }; }这段代码灵气十足。它把 Runnable 的 run() 执行动作放进 Callable.call() 里执行完成后返回预设的 result。这样 FutureTask 的构造器就可以统一处理 Callable 了。你传 Callable直接用你传 Runnable就包一层再存起来。构造器里还有一个针对 Callable 特殊情况的优化如果传入的 Callable 本身是 FutureTask就会尝试直接取出它的 callable。这个优化很少被注意到但它避免了一层无意义的包装嵌套。2. FutureTask 的骨架设计状态字段撑起整个生命周期2.1 状态机从 NEW 到终态的流转FutureTask 内部最核心的就是一个volatile int state字段它记录了任务从创建到结束的完整状态变化。这个字段的值从 0 到 6含义分别是状态值状态名含义0NEW新建状态任务尚未执行1COMPLETING正在设置结果或异常2NORMAL正常完成结果已保存3EXCEPTIONAL任务执行抛异常4CANCELLED任务被取消未中断5INTERRUPTING正在中断执行任务的线程6INTERRUPTED中断完成状态流转只有三条主线正常路径是从 NEW 到 COMPLETING 再到 NORMAL异常路径是 NEW 到 COMPLETING 再到 EXCEPTIONAL取消路径是 NEW 到 CANCELLED如果选择中断线程则经过 INTERRUPTING 再到 INTERRUPTED。这三条路径在代码里分别由 set()、setException()、cancel() 驱动。为什么要在 NORMAL 之前设计一个 COMPLETING 中间态这是为了允许“正在唤醒等待线程”的时候get() 的调用方能够根据状态提前判断如果已经不在 NEW 了直接读结果或抛异常不用再入队等待。COMPLETING 是一个极短暂的过渡状态它主要起到“并发可见性屏障”的作用。不过说实话源码里对 COMPLETING 的使用并不强制 CAS只是简单地赋值因为执行 set 的只有一个线程while 循环里用 state 变量保持可见性就够了。2.2 关键字段outcome, runner 与 waiters除了 stateFutureTask 还有三个关键字段private CallableV callable; // 包装后的任务本体 private Object outcome; // 执行结果或异常对象 private volatile Thread runner; // 正在执行任务的线程 private volatile WaitNode waiters; // 等待结果的线程链表头callable 负责真正干活outcome 负责存结果或异常runner 在任务执行期间被设为当前线程主要服务于 cancel() 的中断逻辑waiters 则是一个 Treiber 栈结构的链表头所有调用 get() 且任务未完成的线程都会封装成一个 WaitNode 挂到链上。outcome 的类型是 Object 而不是 V这一点也可以理解因为异常同样是需要保存的“结果”。当任务正常完成时outcome 保存计算结果任务异常时outcome 保存异常对象任务被取消时outcome 不会被赋值。get() 内部会根据 state 判断如果是 NORMAL就把 outcome 强转为 V 返回如果是 EXCEPTIONAL就把 outcome 包装成 ExecutionException 抛出如果是 CANCELLED 或 INTERRUPTED则抛 CancellationException。runner 字段最妙的一点在于它不是仅仅用于记录而是和 CAS 操作绑定。任务执行的 run() 方法第一步就是通过 Unsafe CAS 把 runner 从 null 修改为当前线程如果 CAS 失败说明已经有线程在执行这个任务了当前线程直接 return。这就是 FutureTask 保证“同一时刻只有一个线程执行任务”的手段。2.3 构造器的隐藏细节FutureTask 有两个构造器public FutureTask(CallableV callable) { if (callable null) throw new NullPointerException(); this.callable callable; this.state NEW; // 显式赋值保证可见性 } public FutureTask(Runnable runnable, V result) { this.callable Executors.callable(runnable, result); this.state NEW; }第二个构造器就是“Runnable 适配为 Callable”的捷径传入 Runnable 和预设结果值内部转成一个返回固定值的 Callable。这意味着 FutureTask 不是只能配合 Callable 使用也能完美兼容老代码里的 Runnable。有很多线程池封装老代码的场景会用到这个构造器比如new FutureTask(task, null)拿到一个不关心结果但想控制取消的任务。state 字段在构造器里被显式赋为 NEW。虽然它是 volatile 的默认值就是 0但显式赋值是给 JIT 和读线程一个更明确的信号避免重排序带来的边界问题。3. 核心适配逻辑run() 到底做了什么3.1 run() 方法的完整时序前面铺垫了这么多终于到核心了。FutureTask 的 run() 本身作为 Runnable 适配入口被 Thread 或线程池调用。方法关键代码大致是这样public void run() { if (state ! NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { CallableV c callable; if (c ! null state NEW) { V result; boolean ran; try { result c.call(); ran true; } catch (Throwable ex) { result null; ran false; setException(ex); } if (ran) set(result); } } finally { runner null; int s state; if (s INTERRUPTING) handlePossibleCancellationInterrupt(s); } }这段代码信息量很大。首先它检查 state 是不是 NEW再用 CAS 把 runner 从 null 设置成当前线程。如果 CAS 失败说明任务已经在其他线程上运行了直接退出避免了重复执行。这里还考虑到了任务被取消的情况如果 state 在设置为 NEW 之后被 cancel() 改成 CANCELLED或者进入 INTERRUPTINGrun() 方法会看到state ! NEW然后返回。但这里有个微妙的点由于 CAS runner 发生在 check state 之后存在一个微小的时间窗口。接下来调用c.call()如果正常返回就调用 set(result) 记录结果如果抛出异常就调用 setException(ex) 记录异常和状态。这两个 set 方法只负责设置 outcome 和改变状态真正“唤醒等待线程”的动作在 finishCompletion() 里统一处理。异常被捕获得极其宽泛catch 的是 Throwable 而不是 Exception因为未来 JVM 可能出现其他 Error 类型不能让 Error 破坏 FutureTask 的状态一致性。最后 finally 块里有三个动作把 runner 置空读取当前状态如果状态是 INTERRUPTING 就调用handlePossibleCancellationInterrupt(s)。这一步是为了处理“线程正在被中断但中断尚未完成”的中间状态。做什么呢实际上是自旋让出 CPU等待 cancel() 线程把状态推进到 INTERRUPTED。这个细节极其冷门我在读源码时差点忽略但它保证了认知一致性如果任务被要求中断必须等中断彻底完成否则线程可能重新开始执行任务引发状态混乱。3.2 结果保存与线程唤醒的拆分设计set 和 setException 的内部实现逻辑很像核心都是修改状态然后触发 finishCompletion。protected void set(V v) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome v; UNSAFE.putOrderedInt(this, stateOffset, NORMAL); finishCompletion(); } } protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome t; UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); finishCompletion(); } }注意这里的次序先 CAS 从 NEW 到 COMPLETING然后赋值 outcome再 putOrderedInt 从 COMPLETING 改为 NORMAL 或 EXCEPTIONAL。putOrderedInt 是弱形式的 volatile 写不保证立刻对其他线程可见但保证不会重排序到 outcome 赋值之前。这种写法的性能比普通 volatile 写略好因为不需要强制内存屏障。对于读线程来说只要能看到 state 变成 NORMAL就一定能看到前面的 outcome 赋值。finishCompletion() 做的事情是遍历 waiters 链表唤醒每一个节点上阻塞的线程然后清空 waiter 链表。这个方法还顺带把 callable 置空让任务对象不再持有底层的 Callable方便 GC 回收。从线程安全角度看它做了两层处理第一层通过自旋 CAS 反复尝试获取 waiters 头节点并将链表头设为 null防止并发 get() 线程不断往链表里添加新节点第二层通过同步块保证同一个节点不会被重复唤醒。3.3 手写一个简化版 FutureTask 理解适配本质源码看多了容易晕我建议你亲手写一个极简版本只保留最核心的“适配 结果存取 等待唤醒”。下面这个简化模型去掉了很多并发优化但足够还原核心流程public class SimpleFutureTaskV implements Runnable { private final CallableV callable; private volatile V result; private volatile boolean done; public SimpleFutureTask(CallableV callable) { this.callable callable; } Override public void run() { try { result callable.call(); } catch (Exception e) { throw new RuntimeException(e); } finally { done true; synchronized (this) { notifyAll(); } } } public V get() throws InterruptedException { synchronized (this) { while (!done) { wait(); } } return result; } }这个版本没有状态机、没有 CAS、没有超时控制但它把 FutureTask 的最小原型讲清楚了run() 把 callable 包起来执行get() 拿到结果。实际 FutureTask 做的事情完全围绕着这个模型只是把同步和状态控制做到了极致的细粒度以便在超高性能并发下依然出色。4. get() 方法与阻塞唤醒机制到底怎么运作4.1 get() 的两种形态public V get() throws InterruptedException, ExecutionException { int s state; if (s COMPLETING) s awaitDone(false, 0L); return report(s); } public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { if (unit null) throw new NullPointerException(); int s state; if (s COMPLETING (s awaitDone(true, unit.toNanos(timeout))) COMPLETING) throw new TimeoutException(); return report(s); }无参 get() 会无限期等待直到任务完成带超时的 get() 会在等待超时后抛出 TimeoutException。判断条件都是state COMPLETING也就是任务还处于 NEW、COMPLETING 这两个未完成状态。为什么连 COMPLETING 也要等待因为 COMPLETING 只是设置结果的瞬间结果可能还没完全写入 memory立刻读取可能读到旧值。这属于状态机给内存可见性上的保险。awaitDone 是阻塞的核心方法它是整个 FutureTask 里最复杂的部分我把它拆成三个关键点讲。4.2 等待链表WaitNode 的组织方式FutureTask 的等待结构是 Treiber Stack一个无锁并发栈。每个调用 get() 且任务未完成的线程会被封装成一个 WaitNode通过 CAS 压入栈顶private int awaitDone(boolean timed, long nanos) throws InterruptedException { long deadline timed ? System.nanoTime() nanos : 0L; WaitNode q null; boolean queued false; for (;;) { if (Thread.interrupted()) { removeWaiter(q); throw new InterruptedException(); } int s state; if (s COMPLETING) { if (q ! null) q.thread null; return s; } else if (s COMPLETING) Thread.yield(); else if (q null) q new WaitNode(); else if (!queued) queued UNSAFE.compareAndSwapObject(this, waitersOffset, q.next waiters, q); else if (timed) { nanos deadline - System.nanoTime(); if (nanos 0L) { removeWaiter(q); return state; } LockSupport.parkNanos(this, nanos); } else LockSupport.park(this); } }这里有几个设计亮点值得单独说。第一首轮循环先检查 Thread.interrupted()区别于 isInterrupted()它会清除中断标志位。如果发现当前线程被中断它会把等待节点从链表上移除并抛出 InterruptedException。这是 FutureTask 对中断语义的严格实现等待 get() 的线程可以被中断退出。第二如果状态是 COMPLETING即另一个线程正在设置结果这里不会入队而是执行 Thread.yield() 让出 CPU。因为 COMPLETING 是瞬时状态让出 CPU 等对方写完比入队唤醒的代价更小。这个优化非常精细。第三等待通过 LockSupport.park / parkNanos 完成而不是 Object.wait。LockSupport 不需要持有锁不会和 synchronized 争用配合 Treiber 栈的 CAS 入队就组成了一个完全无锁的等待唤醒模型。只有当任务达到终态并调用 finishCompletion 时才会通过 UNSAFE.unpark 去唤醒链表上的线程。4.3 唤醒之后report() 如何转换结果当一个等待线程被唤醒awaitDone 的循环会重新读取 state如果发现状态大于 COMPLETING就返回这个状态。然后 get() 调用 report(s)private V report(int s) throws ExecutionException { Object x outcome; if (s NORMAL) return (V)x; if (s CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); }这里的逻辑一眼就能看明白正常完成直接强转并返回结果被取消抛 CancellationException其他异常情况把 outcome 强转为 Throwable包进 ExecutionException 抛出。很多同学第一次看到 ExecutionException 会一头雾水实际业务报错信息明明在 cause 里为什么外层还要包一层因为 get() 本身无法区分“任务执行失败”和“获取结果过程失败”干脆约定只要任务是异常终止的不管什么原因统一抛 ExecutionException真正的异常放在 cause 里。这样调用方的异常处理逻辑就不用关心具体异常类型只需要通过 getCause() 去拆解。建议业务代码里统一这样处理try { Object result futureTask.get(); } catch (CancellationException e) { // 任务被取消 } catch (ExecutionException e) { // 业务异常解包处理 Throwable cause e.getCause(); log.error(Task failed: , cause); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }还有个细节InterruptedException 抛出前awaitDone 已经通过 Thread.interrupted() 清除了中断标记。所以在 catch 里重新执行Thread.currentThread().interrupt()恢复中断标记是很有必要的否则外层调用方会丢失“当前线程曾被中断”这个重要信息这也是容易被忽略的坑。5. 实操演示线程池 FutureTask 的标准用法5.1 不使用线程池的直接执行FutureTask 最常见的两种用法第一种是配合 Thread 直接执行。这种方式的优点是不需要线程池代码简单直接缺点是线程无法复用只适合任务量少的场景。FutureTaskString task new FutureTask(() - { TimeUnit.SECONDS.sleep(2); return Result from thread; }); new Thread(task, worker-thread).start(); String result task.get(); System.out.println(result);new Thread 接收的是 RunnableFutureTask 正好实现了 Runnable所以可以直接传入。这里就看出了适配模式的价值Thread 完全不认识 Callable 和 Future但它认识 Runnable而 FutureTask 是 Runnable 的合法实现。5.2 配合线程池获取返回值第二种是配合线程池使用。ExecutorService.submit(Callable) 内部其实就是把 Callable 包装成 FutureTask 再交给 execute() 执行。不过这里有个值得一提的点看源码会发现它用的是newTaskFor(callable)而不是直接 new FutureTaskprotected T RunnableFutureT newTaskFor(CallableT callable) { return new FutureTaskT(callable); } public T FutureT submit(CallableT task) { if (task null) throw new NullPointerException(); RunnableFutureT ftask newTaskFor(task); execute(ftask); return ftask; }这个 newTaskFor 是 protected 方法意味着子类可以重写它替换成自定义的 RunnableFuture 实现。很多框架就是通过继承 ThreadPoolExecutor 并重写 newTaskFor 来扩展任务跟踪、添加监听回调等功能。比如你可以重写它给每个任务包装一层审计信息再交给真正执行器。在实际项目里我更推荐直接使用 submit() 返回的 Future而不是自己手动 new FutureTask 再交给线程池 execute()。因为 submit() 还顺带帮你做了泛型类型推断和异常封装的统一处理。但如果你的场景是需要同时控制任务的生命周期和传递同一个 FutureTask 给多个地方手动创建 FutureTask 就有优势。5.3 综合示例多任务并行计算并汇总结果放一个稍微贴近业务实际的例子。假设要并行调用三个外部接口最后把结果拼接起来ExecutorService executor Executors.newFixedThreadPool(3); ListFutureTaskString tasks new ArrayList(); tasks.add(new FutureTask(() - queryOrder())); tasks.add(new FutureTask(() - queryUser())); tasks.add(new FutureTask(() - queryProduct())); for (FutureTaskString task : tasks) { executor.submit(task); } ListString results new ArrayList(); for (FutureTaskString task : tasks) { try { results.add(task.get(3, TimeUnit.SECONDS)); } catch (TimeoutException e) { results.add(timeout); task.cancel(true); } catch (ExecutionException e) { results.add(error: e.getCause().getMessage()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } }提交时直接用 execute(task) 或 submit(task) 都可以因为 FutureTask 已经是 RunnableFuture 了。这里我给 get 加了 3 秒超时防止某个接口拖慢整个流程。如果超时了就取消任务避免它继续占用线程资源。这种“先提交所有任务再逐个 get”的模式在并行调用场景里非常常用。6. 常见问题与排查技巧实录6.1 get() 一直阻塞不返回怎么办这是使用 FutureTask 最高频的问题。任务本身可能因为网络等待、死锁、死循环等原因长时间不结束而 get() 在主线程里无限等待导致整个应用卡住。解决办法有两条。第一能用带超时的 get 就别用无参 get。get(3, TimeUnit.SECONDS)至少能保证你有退出机会。第二超时后根据业务决定要不要 cancel(true)。注意 cancel(true) 只是给执行线程发送中断信号如果任务不响应中断比如纯计算任务或者 catch 了 InterruptedException 后继续执行任务还会继续跑下去。cancel(true) 之后get() 会抛 CancellationException但线程池里那个 worker 线程依然被占用着直到任务真正返回。我在一个实际案例里遇到过这种情况任务内部调用了另一个服务的 connection 方法socket 超时设置成了 5 分钟而业务端 get 超时只有 3 秒。3 秒后 get 超时返回主线程继续往下走但 worker 线程还卡在那次连接上。排查时线程池的线程数量一直在增长最后撑爆了连接池。后来把注意力放在梳理任务内部的外部调用超时时间上而不是单纯靠在 get() 这里做文章。6.2 任务抛异常后get() 的表现有同学以为任务里 try-catch 住异常get 就不会抛。其实恰恰相反只要 Callable.call() 里 throw 了一个异常即使你内部 catch 住再抛一个新的FutureTask 都会把它记为异常状态get() 时统一抛 ExecutionException。所以如果你希望任务发生业务异常时 get 不抛错就要在 Callable 内部吞掉异常并返回一个降级值。想拿到真正的异常链必须用 getCause() 逐层解包。习惯性打印异常堆栈的话也要注意直接e.printStackTrace()出来的是 ExecutionException 本身看多了就知道还是要看 cause。6.3 cancel() 之后任务还能执行吗cancel(false) 只把状态从 NEW 改成 CANCELLED任务如果还没开始执行就永远不会执行如果已经执行了它也停不下来。cancel(true) 会额外中断执行线程。但这里有个大坑如果任务是FutureTask.run()恰好还没进入call()方法cancel(true) 会先通过 CAS 把状态改成 INTERRUPTING然后拿到 runner 线程调用 interrupt()。如果在中断信号发出之前任务已经开始执行了很大概率会被中断生效。如果中断信号发出之后任务才进入 call()那根本感知不到中断。所以 cancel(true) 对“任务尚未开始”的场景是有效的阻止执行对“任务已经开始”的场景只是尽力而为。我在线上系统遇到过 cancel 和 run 并发触发导致状态紊乱的情况那是在比较老的 JDK 版本上。后来的版本增加了 handlePossibleCancellationInterrupt 来处理中断未完成的情况。这个问题比较深绝大多数业务场景不需要关心。6.4 FutureTask 在线程池里的内存保留问题如果不取消任务它自己执行完outcome 会保存结果对象直到有线程调用 get() 取走为止。如果任务执行完但结果一直没人取FutureTask 会保留 outcome 引用造成对象迟迟无法被 GC。在批量提交任务的场景下如果每个任务都返回一个较大的对象而调用方并没有及时 get()内存里堆积的对象可能相当可观。优化办法是提交任务后要么在 finally 里统一 get() 取走结果要么确实不需要返回值就提交 Runnable 而不是 Callable。当然JDK 8 后的 CompletableFuture 在结果消费上更灵活也引入了依赖回调的机制不会出现“结果存着没人要”的尴尬局面。6.5 FutureTask 与 CompletableFuture 的取舍现在很多新项目已经逐渐转向 CompletableFuture它提供了任务编排、异步回调、异常恢复等更高级的能力。但 FutureTask 仍然有自己的生态位它简单轻量没有回调地狱也没有 ForkJoinPool 的底层依赖。在只想“拿到一个异步结果 支持取消”的场景FutureTask 反而是最直接的工具。我的建议是单任务异步取结果用 FutureTask 或 Future 即可有多任务编排、依赖传递、异常恢复需求再考虑 CompletableFuture。不要为了用 CompletableFuture 而硬把简单逻辑搞复杂。6.6 一个小技巧futureTask.get() 与锁的关系最后分享一个容易踩的小坑。FutureTask 的 get() 会阻塞等待任务完成如果这个 FutureTask 被提交到了一个线程池而这个线程池又被某把锁挡住比如池里的线程都在等待一个永远不释放的锁那 get() 就会永远等不到结果。这种情况本质上是死锁但表现形式像是 get() 卡住。排查并发问题时候不要只盯着 get()要结合线程 dump 看整个调用链搞清楚是任务本身没结束还是线程池整体被堵住了。我在实际项目里排查过一个“看起来像 FutureTask 卡死”的问题线程 dump 之后发现所有 worker 线程都阻塞在一个数据库连接池的获取上而连接池已经被占满。那根本不是 FutureTask 的问题而是对线程池容量的评估失准。所以说FutureTask 只是工具工具本身很少出错真正容易错的是我们对并发场景的理解是否足够全面。6.7 从源码中能学到的设计思路读 FutureTask 源码最大的收获不只是会用 get()而是欣赏它在状态设计上的严谨一个 int 字段既承载了用户可感知的状态又充当了线程安全的通知信号CAS 和 volatile 的组合保证了不同线程之间对结果和状态的可见性LockSupport 的 park/unpark 避免了锁竞争的额外开销Treiber 栈则让大量等待线程的入队和唤醒操作变成无锁操作。这几套并发原语配合起来整个类的内部几乎看不到一个 synchronized 块除了 finishCompletion 中的一小段却能在多线程高竞争下保持正确的语义。如果你还在纠结 FutureTask 的原理我建议你找个周末坐下来把 awaitDone 那个 for 循环自己画一遍执行线再把 set/cancel/finishCompletion 的状态流转写在一张纸上。画完这张图你对 Java 并发的理解会明显上一个台阶。我是把这张图贴在工位上方之后再看线程池相关的很多问题思路都顺了不少。