Skip to content

Thread / Runnable / Callable / Future

在总览中的位置Java 并发模型总览 §1
相关主题ExecutorService / 线程池 · JMM:happens-before / 安全发布

一句话定位

Thread 是执行载体,Runnable / Callable 是任务描述,Future 是异步结果句柄;这组概念的核心是把“谁来跑、何时拿结果、如何取消、哪里会阻塞”拆开理解。

代码索引

主题Lab 说明源码
submit() / Future.get() / 拒绝策略 / CallerRunsPolicythread-pool-basicsThreadPoolBasics.java

当前仓库还没有“Thread / Future 专属单文件 lab”;本专题先复用 thread-pool-basicssubmit()Future.get()、任务提交与回收逻辑做联动学习。

为什么需要

很多工程师会写 new Thread(),但真实工程里更容易栽在以下问题上:

  • Android:主线程不能阻塞,页面退出后后台任务还在跑,结果回调回来时页面已经销毁。
  • Backend:一个请求往往会并行打多个下游;如果没有返回值建模、超时控制和取消语义,所谓“并发”最后只是更乱的阻塞。
  • Java/Kotlin 混合项目:协程上层看起来优雅,但底层 SDK、老库、线程池、Future 仍然是线上问题来源。
  • Flutter / Web 对照:Flutter isolate 和 Web event loop 默认不共享内存,思维负担更偏“消息传递”;Java 共享内存并发则必须显式考虑任务边界、阻塞点和结果可见性。

最常见的误区是把这些概念混成一句话:

  • Thread 不是任务;它只是跑任务的“车”。
  • Runnable / Callable 不是线程;它们只是“车上装的活”。
  • Future 不是异步魔法;它只是“领结果、查状态、请求取消”的票据。

一个最小对照:

java
// ❌ 直接 new Thread:执行了,但结果、取消、统一治理都要自己补
new Thread(() -> userRepository.refresh()).start();

// ✅ 把任务交给执行器:结果、取消、状态观察都有了统一入口
Future<User> future = pool.submit(() -> api.loadUser(userId));
User user = future.get(300, TimeUnit.MILLISECONDS);

底层机制

1. Thread:最底层执行载体

Thread 代表 JVM 映射到 OS 调度实体的执行单元。直接 new Thread() 意味着你在手动承担:

  • 创建成本:线程栈、调度、上下文切换
  • 生命周期:启动、结束、命名、守护线程属性
  • 异常边界:线程里抛错如何记录、如何上报
  • 取消语义:如何响应中断、如何收尾

最小示例:

java
Thread worker = new Thread(() -> {
    System.out.println("run on " + Thread.currentThread().getName());
}, "profile-loader");
worker.start();
worker.join();

要点:

  • start() 才会真正创建新的执行路径。
  • run() 只是普通方法调用;直接调 run() 不会并发。
  • join() 是一个显式阻塞点;调用线程会等目标线程结束。

什么时候可以直接用 Thread

  • 最小复现 bug
  • 非常底层的并发教学 / 原语实验
  • 少量需要手动命名、守护属性、优先级控制的基础设施代码

什么时候不该直接用?

  • 日常业务任务
  • 一批短任务
  • 需要统一取消、超时、线程命名、监控的场景

2. Runnable:无返回值任务描述

Runnable 解决的是“把要做的事描述出来”,而不是“由谁执行”。

java
Runnable warmupTask = () -> cache.preload();
new Thread(warmupTask, "cache-warmup").start();
pool.execute(warmupTask);

适合:

  • fire-and-forget 任务
  • 结果不需要回收
  • 结果通过其他机制异步通知(如消息、回调、状态流)

边界:

  • 没有返回值
  • 不能抛受检异常
  • 如果你开始依赖外部共享变量回传结果,说明它可能已经不适合继续用 Runnable

3. Callable:带返回值、可抛异常的任务抽象

Callable<V> 更像“异步函数”:

java
Callable<String> tokenTask = () -> {
    String token = authApi.refreshToken();
    if (token == null) {
        throw new IllegalStateException("empty token");
    }
    return token;
};

适合:

  • 有明确返回值
  • 调用方要知道成功/失败
  • 需要把异常留给上层统一处理

工程上,CallableRunnable 更适合:

  • 后台查库 / 查网络返回结果
  • 并行 fan-out 聚合
  • 单次计算任务(压缩、签名、转换)

4. Future:异步结果句柄,不是执行器

Future 的职责只有四类:

  • get():等待结果
  • get(timeout, unit):有超时地等待结果
  • cancel(mayInterruptIfRunning):请求取消
  • isDone() / isCancelled():观察状态
java
Future<String> future = pool.submit(() -> {
    Thread.sleep(200);
    return "done";
});

if (!future.isDone()) {
    System.out.println("still running");
}

String result = future.get(500, TimeUnit.MILLISECONDS);
System.out.println(result);

必须记住两件事:

  1. Future结果句柄,不是任务调度器。
  2. get()阻塞点;放错位置就会把异步重新变回同步。

5. submit()execute()start() 的差异

调用你交出去的是什么有没有返回句柄常见误区
thread.start()一个 Thread没有以为 run()start() 一样
executor.execute(r)Runnable没有任务抛异常后没有统一结果句柄
executor.submit(r/c)Runnable / CallableFuture以为 submit() 自动让错误可见;实际上要 get() 才能显式感知任务异常

选型建议:

  • 只需要扔出去执行、不需要结果:execute() / Runnable
  • 需要返回值、超时、取消、异常观察:submit() / Callable
  • 只有在做底层控制或最小实验时才直接 new Thread()

6. 中断与取消:cancel(true) 不是“强制杀线程”

java
Future<?> future = pool.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        doOneChunk();
    }
});

future.cancel(true);

cancel(true) 的语义是:

  • 尽量给运行中的线程发中断信号
  • 能否停下来取决于任务本身是否检查中断、是否调用可中断阻塞 API

如果任务代码从不检查中断,或者把 InterruptedException 吃掉不处理,那么“取消成功”往往只是表面成功。

Android / Flutter / Web / Backend 对照

维度Java 线程 / FutureAndroidFlutterWebBackend
执行载体Thread / ExecutorService主线程 + 线程池 + HandlerThreadmain isolate + child isolateevent loop + Workerrequest thread / 业务线程池
任务描述Runnable / CallableRunnable、协程桥接到线程池Futurecompute()Isolate.spawnpromise / task / callbackCallableCompletableFuture
结果回收Future.get()严禁在主线程直接 get()await 非阻塞主 isolate 调度await 回调到 event loop聚合多个下游结果
取消方式cancel(true) + 中断配合页面销毁 / ViewModel 清理时取消StreamSubscription.cancel / isolate killAbortController请求超时、熔断、取消传播
心智负担共享内存 + 阻塞点管理额外叠加生命周期 / ANR更偏消息隔离更偏事件顺序更偏资源治理、超时和尾延迟

常见场景

1. Android 页面退出后取消后台任务

比如详情页里点“导出图片元数据”,任务会跑几百毫秒到几秒。如果页面已经返回,继续计算再回调 UI 就没有意义了。

java
class DetailPresenter {
    private Future<?> exportFuture;

    void export(ExecutorService pool) {
        exportFuture = pool.submit(() -> exporter.exportCurrentItem());
    }

    void onDestroy() {
        if (exportFuture != null) {
            exportFuture.cancel(true);
        }
    }
}

排障点:

  • 如果取消后 CPU 还在跑,先检查任务内部是否响应中断。
  • 如果 cancel(true) 后仍然写文件,说明循环体 / IO 路径没有及时检查中断标记。

2. Backend 并行调用多个下游再聚合

java
Future<User> userFuture = pool.submit(() -> userApi.queryUser(uid));
Future<OrderSummary> orderFuture = pool.submit(() -> orderApi.querySummary(uid));

User user = userFuture.get(200, TimeUnit.MILLISECONDS);
OrderSummary summary = orderFuture.get(200, TimeUnit.MILLISECONDS);

适合:

  • 两个下游彼此独立
  • 你需要总超时和降级策略

边界:

  • 不要在同一个很小的固定线程池里“任务 A 再等任务 B”,很容易做成线程池饥饿。
  • 对多依赖聚合场景,超过 2~3 条链后,CompletableFuture 或协程通常比裸 Future 更易维护。

3. 底层 SDK / 老 Java 模块里的一次性初始化任务

当项目还没完全迁到 Kotlin 协程、或者你在维护 Java SDK / Java 服务端模块时,经常需要把一个预热任务交给后台跑,但不想把“任务是什么”与“线程怎么建”写死在一起。

java
Runnable preload = () -> dictionary.loadFromDisk();
pool.execute(preload);

这时 Runnable 的价值不在语法,而在于:

  • 可以在测试中替换执行器
  • 可以统一打线程名、超时、监控
  • 可以在上层切换为串行池 / 并行池而不用改任务本体

常见坑

现象修法
run() 当成 start()代码“能跑”,但其实还在当前线程串行执行需要并发时必须调 start() 或交给执行器
主线程直接 Future.get()Android 卡顿、ANR;桌面 UI 卡死改成回调、超时等待、后台线程聚合
在线程池任务内部互相 get()固定线程池看起来“卡死”拆池、扩大并发模型、避免池内互等
只会 cancel(true),任务却不响应中断状态显示取消了,CPU / IO 还在跑任务里检查 isInterrupted(),妥善处理 InterruptedException
用共享可变字段传结果结果竞争、可见性混乱需要结果时优先用 Callable / Future
submit() 后不 get() 也不记日志任务异常悄悄丢在后台统一包装日志,或在关键路径显式 get() / 超时处理
大量直接 new Thread()线程数失控,排障困难换成统一线程池治理

与相近概念对比

概念它是什么解决什么问题不解决什么
Thread执行载体真正把代码跑起来任务治理、结果回收、资源控制
Runnable无返回值任务描述动作返回值、受检异常
Callable<V>有返回值任务描述一次异步计算调度策略
Future<V>结果句柄等待/取消/查状态线程创建、任务编排
FutureTask<V>Runnable + Future 桥接实现需要手动包一层可执行 Future复杂异步编排
CompletableFuture<V>可组合的 future多任务编排、then/apply/exceptionally线程池参数治理本身

对应实验

Lab说明源码
thread-pool-basicsthread-pool-basicsThreadPoolBasics.java

当前仍缺:Thread / Runnable / Callable / Future 专属实验(例如 run() vs start()submit() vs execute()Future.cancel(true) 响应中断演示、池内互相 get() 饥饿复现)。

复习检查题

  1. ThreadRunnableCallableFuture 各自扮演什么角色?

    Thread 是执行载体;Runnable 是无返回值任务描述;Callable 是有返回值且可抛异常的任务描述;Future 是任务提交之后的结果句柄,用来等待、取消和查状态。

  2. 为什么说 Future.get() 是并发代码里的危险阻塞点?

    :因为它会让当前线程阻塞等待结果。如果放在 Android 主线程、请求关键路径、或者小线程池内部互等路径里,就会把异步重新变成卡顿、超时或线程池饥饿问题。

  3. cancel(true) 为什么经常“看起来成功,任务却没停”?

    :因为 cancel(true) 只是发中断请求,不会强杀线程。任务必须自己检查中断标记,或者在可中断阻塞点上正确处理 InterruptedException,才能真正停下来。

  4. 什么场景下 Runnable 不够用,应该换成 Callable

    :当任务需要返回结果、把异常显式传回调用方、或者调用方需要用 Future 做超时/取消/结果回收时,应该优先使用 Callable

  5. run()start() 的根本差异是什么?

    run() 只是普通方法调用,仍在当前线程执行;start() 才会让 JVM 创建新的执行路径并异步调度 run()

速记

  • Thread 是车,Runnable/Callable 是活,Future 是领结果的票。
  • get() 是阻塞点;主线程和小线程池里最危险。
  • 需要结果/异常/取消就上 Callable + Future;只扔任务可用 Runnable
  • cancel(true) 依赖任务自己响应中断。