Skip to content

Callable、Future 与 FutureTask 全过程

Callable 描述“将来会返回结果或异常的任务”,Future 是调用方持有的结果句柄,FutureTask 则把“可执行任务”和“未来结果”组合在一个对象里。真正掌握它们,不只是会 submit().get(),还要理解状态机、等待与唤醒、异常为什么会被藏起来、取消为何不等于杀死线程,以及怎样避免线程饥饿死锁。

学习目标

  • 分清 RunnableCallableFutureRunnableFutureFutureTask
  • 讲清 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 -> NORMALcall() 正常返回;
  • NEW -> COMPLETING -> EXCEPTIONALcall() 抛异常;
  • NEW -> CANCELLEDcancel(false) 成功;
  • NEW -> INTERRUPTING -> INTERRUPTEDcancel(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,需要在 RunnableFuture 时调用 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 同时实现 RunnableFuture:执行方只看 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.orTimeoutcompleteOnTimeout,它们是 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 并分别处理 ExecutionExceptionCancellationException

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