Skip to content

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 异常丢失、超时后任务仍运行和同池父子任务饥饿死锁。

一、五个接口和类的关系 ​

mermaid
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 方法签名决定语义 ​

java
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>获取 Vget 包装任务异常观察、等待、取消结果
RunnableFuture<V>同时具备同时具备连接执行端与结果端
FutureTask<V>同时具备同时具备JDK 的典型 RunnableFuture 实现

Future 不是线程,也不负责决定任务在哪执行。它只是共享状态和结果的访问入口。任务可能在线程池、普通线程中执行,甚至尚未执行。

二、从 submit 到 get 的完整链路 ​

以 ThreadPoolExecutor 为例,主链路可简化为:

mermaid
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 --> J

AbstractExecutorService.submit(Callable) 的核心思路是:

java
public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> future = newTaskFor(task);
    execute(future);
    return future;
}

这解释了两个高频问题:

  1. submit 最终仍通过 execute 把一个可运行对象交给线程池;
  2. 工作线程执行的是 FutureTask.run(),由它捕获结果或异常,因此异常通常不会直接从工作线程冒出去。

三、JDK 7/8 可运行基础 Demo ​

JDK 7 没有 Lambda,使用匿名内部类:

java
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 的阻塞式结果模型没有改变:

java
Future<Integer> future = pool.submit(() -> Integer.valueOf(42));

四、FutureTask 状态机原理 ​

JDK 7/8 的 FutureTask 用一个 volatile int state 表示状态。源码中的核心状态是:

text
NEW          = 0
COMPLETING   = 1
NORMAL       = 2
EXCEPTIONAL  = 3
CANCELLED    = 4
INTERRUPTING = 5
INTERRUPTED  = 6

4.1 合法状态迁移 ​

mermaid
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 清理和中断竞态处理:

java
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 等待链路 ​

mermaid
flowchart TD
    A["调用get"] --> B{"状态已完成吗"}
    B -- "是" --> C["报告结果异常或取消"]
    B -- "否" --> D["把当前线程加入等待者链"]
    D --> E["LockSupport.park挂起"]
    E --> F["任务完成或取消"]
    F --> G["finishCompletion遍历等待者"]
    G --> H["unpark唤醒线程"]
    H --> B

FutureTask 并不是 while 空转消耗 CPU。未完成时,等待线程被组织在等待者链中并通过 LockSupport.park 挂起;任务进入终态后,finishCompletion 唤醒等待者。被唤醒后仍要重新检查状态,因为唤醒、中断、超时之间可能竞争。

6.2 多个线程能否 get 同一个 Future ​

可以。多个调用者会分别加入等待链,完成后都被唤醒,正常情况下都读到同一个结果。get() 不会“取走”结果。

6.3 四种异常不要混淆 ​

异常谁发生了什么处理重点
ExecutionException任务本身失败读取 getCause(),按业务异常处理
TimeoutException本次等待超过期限任务可能仍在运行;决定是否取消
InterruptedException等待 get 的线程被中断退出或恢复中断标记,不要吞掉
CancellationExceptionFuture 已被成功取消这是运行时异常,按取消分支处理

isDone() 在正常、异常、取消三种终态都返回 true,不能用它判断“任务成功”。

七、超时不等于任务停止 ​

java
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) ​

mermaid
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 --> I
  • cancel(false):若成功,Future 进入取消态,不主动中断正在运行线程;若任务其实已开始,任务代码可能继续运行,但结果不会再由这个 Future 正常交付。
  • cancel(true):若成功,还会尝试中断 FutureTask 记录的 runner。
  • 返回 true 只表示 Future 状态成功转为取消,不表示业务动作已经停止或回滚。

8.1 中断是合作协议 ​

java
public String call() throws Exception {
    while (!Thread.currentThread().isInterrupted()) {
        doOneSmallBatch();
    }
    throw new InterruptedException("cancelled");
}

Thread.interrupt() 通常只是设置中断标记;sleep/wait/join 等可中断阻塞会抛 InterruptedException 并清除标记。正确写法是在不能继续处理时退出,或在向上包装异常前恢复标记:

java
try {
    queue.take();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

若任务死循环从不检查标记,或底层调用不响应中断,cancel(true) 也不能强杀。Java 不安全地强杀线程可能让锁、文件和业务状态处于不一致状态,所以废弃了 Thread.stop()。

九、execute 与 submit 的异常为什么不同 ​

mermaid
flowchart TD
    A["execute Runnable"] --> B["工作线程直接调用run"]
    B --> C["未捕获异常交给UncaughtExceptionHandler"]
    D["submit任务"] --> E["FutureTask.run调用任务"]
    E --> F["捕获Throwable并保存"]
    F --> G["调用get才抛ExecutionException"]

如果使用 submit 做“只管提交”的异步任务,却既不保存 Future 也不在任务内部记录异常,任务失败就可能表现为“没有结果、没有错误日志”。

java
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 执行 ​

java
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 按完成顺序消费 ​

java
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。任务很多时仍需控制批次、队列和下游并发。

十二、同线程池父任务等子任务:饥饿死锁 ​

mermaid
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。

mermaid
flowchart TD
    A["订单详情请求"] --> B["计算统一截止时间"]
    B --> C["提交订单查询"]
    B --> D["提交支付查询"]
    B --> E["提交物流查询"]
    C --> F["按剩余预算等待"]
    D --> F
    E --> F
    F --> G{"核心依赖成功吗"}
    G -- "是" --> H["合并结果"]
    G -- "否" --> I["取消剩余任务并返回明确错误"]

核心原则:

  1. 先全部提交,再等待;提交一个就立即 get 会退化为串行。
  2. 使用统一截止时间。若每个 Future 都等 800ms,总耗时可能变成 2400ms。
  3. 线程池并发不应超过数据库连接池、HTTP 连接池和下游承载能力。
  4. 订单主数据是核心依赖,不能超时后伪造成功;物流可按业务约定降级。
  5. 超时取消后,底层 HTTP 客户端也必须有超时和连接释放机制。

JDK 8 中复杂聚合更适合 CompletableFuture 全过程,但 Future 的异常、取消、线程池和 deadline 原理仍然成立。

十四、生产故障 Runbook ​

14.1 接口卡在 Future.get ​

  1. 从告警取得接口、时间范围、traceId、线程池名称;
  2. 连续获取多份 jstack,确认线程是否长期停在 FutureTask.get/awaitDone;
  3. 找对应线程池的 active、pool size、queue size、completed、reject 指标;
  4. 查看执行任务的线程栈究竟卡在数据库连接、socketRead、锁还是 CPU;
  5. 核对父任务是否向同一个线程池提交子任务并等待;
  6. 核对底层依赖超时是否小于上层 deadline;
  7. 先限流/隔离/降级止损,再修复线程池拓扑或下游瓶颈。

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 按完成顺序消费结果。