Callable、Future 与 FutureTask 全过程
Callable 描述“将来会返回结果或异常的任务”,Future 是调用方持有的结果句柄,FutureTask 则把“可执行任务”和“未来结果”组合在一个对象里。真正掌握它们,不只是会 submit().get(),还要理解状态机、等待与唤醒、异常为什么会被藏起来、取消为何不等于杀死线程,以及怎样避免线程饥饿死锁。
学习目标
- 分清
Runnable、Callable、Future、RunnableFuture和FutureTask; - 讲清
submit如何把任务包装成FutureTask并由工作线程执行; - 理解 JDK 7/8
FutureTask状态机,以及run/get/cancel的竞争; - 正确处理正常结果、业务异常、等待超时、等待线程中断和任务取消;
- 明白
cancel(true)只是协作式中断,无法强杀线程或回滚业务; - 用
CompletionService避免按提交顺序get导致的队头阻塞; - 定位
submit异常丢失、超时后任务仍运行和同池父子任务饥饿死锁。
一、五个接口和类的关系
flowchart TD
A["Runnable定义无返回任务"] --> D["线程或线程池执行"]
B["Callable定义有返回任务"] --> E["FutureTask包装"]
E --> D
E --> F["实现RunnableFuture"]
F --> G["Runnable能力"]
F --> H["Future能力"]
H --> I["get cancel isDone"]1.1 方法签名决定语义
public interface Runnable {
void run();
}
public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}| 类型 | 返回值 | 可直接声明受检异常 | 主要职责 |
|---|---|---|---|
Runnable | 无 | 不可以 | 描述一段动作 |
Callable<V> | V | 可以 | 描述一次有结果计算 |
Future<V> | 获取 V | get 包装任务异常 | 观察、等待、取消结果 |
RunnableFuture<V> | 同时具备 | 同时具备 | 连接执行端与结果端 |
FutureTask<V> | 同时具备 | 同时具备 | JDK 的典型 RunnableFuture 实现 |
Future 不是线程,也不负责决定任务在哪执行。它只是共享状态和结果的访问入口。任务可能在线程池、普通线程中执行,甚至尚未执行。
二、从 submit 到 get 的完整链路
以 ThreadPoolExecutor 为例,主链路可简化为:
flowchart TD
A["调用submit Callable"] --> B["newTaskFor创建FutureTask"]
B --> C["execute提交FutureTask"]
C --> D["进入工作线程或任务队列"]
D --> E["Worker调用FutureTask.run"]
E --> F["内部调用Callable.call"]
F --> G{"正常返回还是抛异常"}
G -- "正常" --> H["保存结果并置NORMAL"]
G -- "异常" --> I["保存异常并置EXCEPTIONAL"]
H --> J["唤醒get等待者"]
I --> JAbstractExecutorService.submit(Callable) 的核心思路是:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> future = newTaskFor(task);
execute(future);
return future;
}这解释了两个高频问题:
submit最终仍通过execute把一个可运行对象交给线程池;- 工作线程执行的是
FutureTask.run(),由它捕获结果或异常,因此异常通常不会直接从工作线程冒出去。
三、JDK 7/8 可运行基础 Demo
JDK 7 没有 Lambda,使用匿名内部类:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableJdk7Demo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
try {
Future<Integer> future = pool.submit(new Callable<Integer>() {
public Integer call() throws Exception {
Thread.sleep(300L);
return Integer.valueOf(40 + 2);
}
});
try {
System.out.println("result=" + future.get());
} catch (ExecutionException e) {
System.err.println("task failed: " + e.getCause());
}
} finally {
pool.shutdown();
}
}
}JDK 8 只是可以把 Callable 写成 Lambda,Future 的阻塞式结果模型没有改变:
Future<Integer> future = pool.submit(() -> Integer.valueOf(42));四、FutureTask 状态机原理
JDK 7/8 的 FutureTask 用一个 volatile int state 表示状态。源码中的核心状态是:
NEW = 0
COMPLETING = 1
NORMAL = 2
EXCEPTIONAL = 3
CANCELLED = 4
INTERRUPTING = 5
INTERRUPTED = 64.1 合法状态迁移
flowchart TD
A["NEW"] --> B["COMPLETING"]
B --> C["NORMAL"]
B --> D["EXCEPTIONAL"]
A --> E["CANCELLED"]
A --> F["INTERRUPTING"]
F --> G["INTERRUPTED"]NEW -> COMPLETING -> NORMAL:call()正常返回;NEW -> COMPLETING -> EXCEPTIONAL:call()抛异常;NEW -> CANCELLED:cancel(false)成功;NEW -> INTERRUPTING -> INTERRUPTED:cancel(true)成功并尝试中断运行线程。
状态只能完成一次。多个线程同时调用 run(),只有成功占有执行权的线程会调用 Callable;任务一旦完成或取消,再次 run() 不会重新计算。这就是 FutureTask 的一次性语义。
4.2 为什么需要中间状态
COMPLETING 表示正在发布结果或异常,随后才进入终态;等待线程看见终态时应能看见已写入的结果。INTERRUPTING 表示取消线程正在对执行线程调用 interrupt(),完成后进入 INTERRUPTED。这些中间状态避免观察者过早把“正在发布”误认成“已经全部完成”。
源码使用 volatile、CAS 和内存屏障建立可见性。调用方无需再给 get() 外包一层 synchronized。
五、run 怎样保存结果和异常
可把 run 理解成以下伪代码,真实源码还有 CAS、runner 清理和中断竞态处理:
public void run() {
if (state != NEW || !casRunner(null, Thread.currentThread())) {
return;
}
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
try {
V result = c.call();
set(result);
} catch (Throwable ex) {
setException(ex);
}
}
} finally {
runner = null;
// 处理与cancel(true)并发时的中断状态
}
}正常结果和异常都会保存到 FutureTask 的结果字段中。get() 再根据终态解释该字段:正常态返回值,异常态抛 ExecutionException,取消态抛 CancellationException。
六、get 为什么阻塞,完成后怎么唤醒
6.1 等待链路
flowchart TD
A["调用get"] --> B{"状态已完成吗"}
B -- "是" --> C["报告结果异常或取消"]
B -- "否" --> D["把当前线程加入等待者链"]
D --> E["LockSupport.park挂起"]
E --> F["任务完成或取消"]
F --> G["finishCompletion遍历等待者"]
G --> H["unpark唤醒线程"]
H --> BFutureTask 并不是 while 空转消耗 CPU。未完成时,等待线程被组织在等待者链中并通过 LockSupport.park 挂起;任务进入终态后,finishCompletion 唤醒等待者。被唤醒后仍要重新检查状态,因为唤醒、中断、超时之间可能竞争。
6.2 多个线程能否 get 同一个 Future
可以。多个调用者会分别加入等待链,完成后都被唤醒,正常情况下都读到同一个结果。get() 不会“取走”结果。
6.3 四种异常不要混淆
| 异常 | 谁发生了什么 | 处理重点 |
|---|---|---|
ExecutionException | 任务本身失败 | 读取 getCause(),按业务异常处理 |
TimeoutException | 本次等待超过期限 | 任务可能仍在运行;决定是否取消 |
InterruptedException | 等待 get 的线程被中断 | 退出或恢复中断标记,不要吞掉 |
CancellationException | Future 已被成功取消 | 这是运行时异常,按取消分支处理 |
isDone() 在正常、异常、取消三种终态都返回 true,不能用它判断“任务成功”。
七、超时不等于任务停止
try {
String value = future.get(800, java.util.concurrent.TimeUnit.MILLISECONDS);
System.out.println(value);
} catch (java.util.concurrent.TimeoutException e) {
boolean accepted = future.cancel(true);
System.err.println("wait timeout, cancel accepted=" + accepted);
}get(timeout) 只限制当前线程等待 Future 的时长。超时发生时:
- FutureTask 可能仍在队列中;
- 可能正在执行 CPU 逻辑;
- 可能阻塞在不响应中断的旧式 I/O 或第三方驱动;
- 下游业务可能已经成功,只是响应没有回来。
因此,商业调用要同时配置:上层总 deadline、HTTP/RPC 连接与读取超时、线程池隔离、业务幂等。只给 Future.get 加超时无法释放底层连接,也无法撤销已经提交的订单。
八、cancel(false) 与 cancel(true)
flowchart TD
A["调用cancel"] --> B{"仍是NEW吗"}
B -- "否" --> C["返回false无法改变终态"]
B -- "是" --> D{"mayInterruptIfRunning"}
D -- "false" --> E["置CANCELLED"]
D -- "true" --> F["置INTERRUPTING"]
F --> G["若runner存在则interrupt"]
G --> H["置INTERRUPTED"]
E --> I["唤醒等待者"]
H --> Icancel(false):若成功,Future 进入取消态,不主动中断正在运行线程;若任务其实已开始,任务代码可能继续运行,但结果不会再由这个 Future 正常交付。cancel(true):若成功,还会尝试中断 FutureTask 记录的 runner。- 返回
true只表示 Future 状态成功转为取消,不表示业务动作已经停止或回滚。
8.1 中断是合作协议
public String call() throws Exception {
while (!Thread.currentThread().isInterrupted()) {
doOneSmallBatch();
}
throw new InterruptedException("cancelled");
}Thread.interrupt() 通常只是设置中断标记;sleep/wait/join 等可中断阻塞会抛 InterruptedException 并清除标记。正确写法是在不能继续处理时退出,或在向上包装异常前恢复标记:
try {
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}若任务死循环从不检查标记,或底层调用不响应中断,cancel(true) 也不能强杀。Java 不安全地强杀线程可能让锁、文件和业务状态处于不一致状态,所以废弃了 Thread.stop()。
九、execute 与 submit 的异常为什么不同
flowchart TD
A["execute Runnable"] --> B["工作线程直接调用run"]
B --> C["未捕获异常交给UncaughtExceptionHandler"]
D["submit任务"] --> E["FutureTask.run调用任务"]
E --> F["捕获Throwable并保存"]
F --> G["调用get才抛ExecutionException"]如果使用 submit 做“只管提交”的异步任务,却既不保存 Future 也不在任务内部记录异常,任务失败就可能表现为“没有结果、没有错误日志”。
Future<?> future = pool.submit(new Runnable() {
public void run() {
throw new IllegalStateException("inventory update failed");
}
});
try {
future.get();
} catch (ExecutionException e) {
System.err.println("async failed: " + e.getCause());
}扩展 ThreadPoolExecutor.afterExecute 时也要注意:submit 传入的 Throwable 参数通常为 null,需要在 Runnable 是 Future 时调用 get() 提取异常;同时避免无限等待,因为 afterExecute 处理的是已执行结束的任务。相关完整实现见 线程池生命周期与线上排查。
十、FutureTask 可直接被 Thread 执行
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class FutureTaskDemo {
public static void main(String[] args) throws Exception {
FutureTask<String> task = new FutureTask<String>(new Callable<String>() {
public String call() throws Exception {
return "payment-query-success";
}
});
new Thread(task, "payment-query").start();
System.out.println(task.get());
}
}原因是 FutureTask 同时实现 Runnable 和 Future:执行方只看 run(),调用方只看 get()。生产代码通常仍使用受控线程池,而不是每个任务 new Thread。
十一、批量任务为什么要 CompletionService
11.1 按提交顺序 get 的队头阻塞
假设任务耗时依次为 10 秒、100 毫秒、200 毫秒。按 Future 列表顺序 get() 时,先等 10 秒,后两个早已完成的结果也无法及时处理。这是结果消费的队头阻塞,不代表任务没有并行。
11.2 按完成顺序消费
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletionServiceDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
ExecutorCompletionService<String> completion =
new ExecutorCompletionService<String>(pool);
try {
for (int i = 1; i <= 3; i++) {
final int shard = i;
completion.submit(new Callable<String>() {
public String call() throws Exception {
Thread.sleep((4 - shard) * 100L);
return "shard-" + shard;
}
});
}
for (int i = 0; i < 3; i++) {
System.out.println(completion.take().get());
}
} finally {
pool.shutdown();
}
}
}ExecutorCompletionService 内部用完成队列保存已结束 Future。take() 获得下一个完成的任务,而不是下一个提交的任务。若业务有总超时,应使用带截止时间的 poll,并取消剩余任务,不能对每个任务都重新等待完整超时。
11.3 invokeAll 与 invokeAny
invokeAll提交一组任务并等待全部完成,返回列表顺序与输入任务顺序一致;带超时版本到期会尝试取消未完成任务。invokeAny返回任意一个成功完成的结果,其他任务会被取消;若全部失败则抛异常。- 两者都是阻塞式批量 API。任务很多时仍需控制批次、队列和下游并发。
十二、同线程池父任务等子任务:饥饿死锁
flowchart TD
A["固定线程池只有2个线程"] --> B["两个父任务占满线程"]
B --> C["每个父任务向同池提交子任务"]
C --> D["子任务进入队列"]
B --> E["父任务调用Future.get等待"]
D --> F["没有空闲线程执行子任务"]
E --> F
F --> G["永久等待或直到超时"]这类问题不一定被 JVM 死锁检测器报告,因为没有两个 synchronized 锁构成环;线程栈常显示工作线程都停在 FutureTask.get,而队列中还有任务。
治理方式:
- 不要在同一个有界小线程池中同步等待子任务;
- 拆分父任务与子任务线程池,并按各自下游容量限流;
- 改成异步编排,JDK 8 可使用
CompletableFuture; - 所有等待设置 deadline,但超时只是止损,不是结构性修复。
十三、商业场景:订单详情并行聚合
需求:订单基础信息、支付信息、物流信息可并行查询,总接口预算 800ms。
flowchart TD
A["订单详情请求"] --> B["计算统一截止时间"]
B --> C["提交订单查询"]
B --> D["提交支付查询"]
B --> E["提交物流查询"]
C --> F["按剩余预算等待"]
D --> F
E --> F
F --> G{"核心依赖成功吗"}
G -- "是" --> H["合并结果"]
G -- "否" --> I["取消剩余任务并返回明确错误"]核心原则:
- 先全部提交,再等待;提交一个就立即
get会退化为串行。 - 使用统一截止时间。若每个 Future 都等 800ms,总耗时可能变成 2400ms。
- 线程池并发不应超过数据库连接池、HTTP 连接池和下游承载能力。
- 订单主数据是核心依赖,不能超时后伪造成功;物流可按业务约定降级。
- 超时取消后,底层 HTTP 客户端也必须有超时和连接释放机制。
JDK 8 中复杂聚合更适合 CompletableFuture 全过程,但 Future 的异常、取消、线程池和 deadline 原理仍然成立。
十四、生产故障 Runbook
14.1 接口卡在 Future.get
- 从告警取得接口、时间范围、traceId、线程池名称;
- 连续获取多份
jstack,确认线程是否长期停在FutureTask.get/awaitDone; - 找对应线程池的 active、pool size、queue size、completed、reject 指标;
- 查看执行任务的线程栈究竟卡在数据库连接、
socketRead、锁还是 CPU; - 核对父任务是否向同一个线程池提交子任务并等待;
- 核对底层依赖超时是否小于上层 deadline;
- 先限流/隔离/降级止损,再修复线程池拓扑或下游瓶颈。
14.2 submit 后任务“静默失败”
检查 Future 是否被丢弃、是否调用 get、任务内部是否有统一异常日志、afterExecute 是否正确解包 Future。根据任务 ID 追踪提交数、成功数、失败数,不能只看线程池“执行完成数”,因为异常结束也算完成。
14.3 cancel(true) 后任务仍然运行
检查 cancel 返回值、任务是否已进入终态、任务循环是否检查中断、是否吞掉 InterruptedException、第三方 I/O 是否响应中断。若外部系统已接收请求,需要用业务请求 ID 查询最终状态和补偿,不能再次无脑提交。
14.4 线程池队列增长但 CPU 不高
通常不是“CPU 不够”,而是任务在等待数据库连接、下游网络或同池子任务。结合线程栈、连接池等待、下游 P99 和队列最老任务年龄判断。盲目加线程会增加排队和下游压力。
十五、JDK 版本差异
| 版本 | 与本章相关能力 |
|---|---|
| JDK 5 | 引入 java.util.concurrent、Callable、Future、FutureTask、ExecutorService |
| JDK 7 | 上述机制成熟;代码通常使用匿名内部类 |
| JDK 8 | 引入 Lambda 和 CompletableFuture;FutureTask 核心语义仍相同 |
| JDK 9+ | CompletableFuture 增加超时等便利 API;不能反推 JDK 8 可直接使用 |
| JDK 21 | 虚拟线程降低阻塞线程成本,但 Future 取消、deadline 和下游容量问题仍存在 |
JDK 8 没有 CompletableFuture.orTimeout 和 completeOnTimeout,它们是 JDK 9 增加的。JDK 8 项目通常用调度器、底层客户端超时或封装统一 deadline,不能复制高版本代码后声称兼容 JDK 8。
十六、常见面试题标准回答
Callable 和 Runnable 有什么区别
Runnable.run() 返回 void 且不能直接声明受检异常;Callable<V>.call() 返回 V 并可抛 Exception。线程池提交 Callable 后通常把它包装为 FutureTask,Future 负责获取结果、观察完成和尝试取消。
Future.get 有哪些风险
无参 get 会无限阻塞;在同池父子任务结构中还可能造成线程饥饿死锁。生产应设置统一 deadline、配置底层 I/O 超时、监控线程池,并避免在同一小线程池中同步等待子任务。
cancel(true) 能保证任务停止吗
不能。它在成功改变 Future 状态后尝试中断执行线程。中断是合作机制,任务必须检查中断或调用可中断阻塞;不响应中断的代码可能继续执行,已经发生的数据库或远程业务也不会自动回滚。
isDone 为 true 是否表示执行成功
不是。正常完成、异常完成和取消都会使 isDone() 为 true。要判断结果,需调用 get 并分别处理 ExecutionException 和 CancellationException。
FutureTask 的作用是什么
FutureTask 实现 RunnableFuture,同时是 Runnable 和 Future。线程或线程池可执行它,调用方可等待结果。它用一次性状态机协调运行、完成、异常、取消以及多个等待线程,并在完成时唤醒等待者。
CompletionService 解决什么问题
它把完成的 Future 放入完成队列,让调用方按完成顺序获取结果,避免按提交顺序 get 时被最慢的首个任务阻塞,适合批量查询、分片处理和先完成先消费。
十七、常见误区与后果
| 误区 | 真相 | 后果 |
|---|---|---|
get(timeout) 会停止任务 | 只停止本次等待 | 线程和下游调用继续占资源 |
cancel(true) 会杀线程 | 只尝试中断 | 任务继续写库或调接口 |
isDone 等于成功 | 取消和异常也算 done | 业务把失败当成功 |
submit 异常会自动打印 | 异常通常存入 Future | 任务静默失败 |
| 多个 Future 逐个 get 就一定高效 | 首个慢任务会阻塞结果消费 | 快结果无法及时处理 |
| 线程池内再 submit 到同池并 get 没问题 | 可能耗尽所有工作线程 | 线程饥饿死锁 |
十八、关联知识与验收清单
完成以下任务才算掌握:
- 不看文档画出 submit、FutureTask.run、Callable.call、get 的链路;
- 能解释七个状态及三组合法迁移;
- 能写 JDK 7 匿名内部类版本并用
javac --release 8编译; - 能演示
ExecutionException.getCause()和TimeoutException; - 能证明超时后任务仍可能继续运行;
- 能构造并解释同一两线程池中的父子任务饥饿死锁;
- 能用 CompletionService 按完成顺序消费结果。
