Appearance
Thread / Runnable / Callable / Future
在总览中的位置:Java 并发模型总览 §1
相关主题:ExecutorService / 线程池 · JMM:happens-before / 安全发布
一句话定位
Thread 是执行载体,Runnable / Callable 是任务描述,Future 是异步结果句柄;这组概念的核心是把“谁来跑、何时拿结果、如何取消、哪里会阻塞”拆开理解。
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
submit() / Future.get() / 拒绝策略 / CallerRunsPolicy | thread-pool-basics | ThreadPoolBasics.java |
当前仓库还没有“Thread / Future 专属单文件 lab”;本专题先复用
thread-pool-basics中submit()、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;
};适合:
- 有明确返回值
- 调用方要知道成功/失败
- 需要把异常留给上层统一处理
工程上,Callable 比 Runnable 更适合:
- 后台查库 / 查网络返回结果
- 并行 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);必须记住两件事:
Future是结果句柄,不是任务调度器。get()是阻塞点;放错位置就会把异步重新变回同步。
5. submit()、execute()、start() 的差异
| 调用 | 你交出去的是什么 | 有没有返回句柄 | 常见误区 |
|---|---|---|---|
thread.start() | 一个 Thread | 没有 | 以为 run() 跟 start() 一样 |
executor.execute(r) | Runnable | 没有 | 任务抛异常后没有统一结果句柄 |
executor.submit(r/c) | Runnable / Callable | Future | 以为 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 线程 / Future | Android | Flutter | Web | Backend |
|---|---|---|---|---|---|
| 执行载体 | Thread / ExecutorService | 主线程 + 线程池 + HandlerThread | main isolate + child isolate | event loop + Worker | request thread / 业务线程池 |
| 任务描述 | Runnable / Callable | Runnable、协程桥接到线程池 | Future、compute()、Isolate.spawn | promise / task / callback | Callable、CompletableFuture |
| 结果回收 | Future.get() | 严禁在主线程直接 get() | await 非阻塞主 isolate 调度 | await 回调到 event loop | 聚合多个下游结果 |
| 取消方式 | cancel(true) + 中断配合 | 页面销毁 / ViewModel 清理时取消 | StreamSubscription.cancel / isolate kill | AbortController | 请求超时、熔断、取消传播 |
| 心智负担 | 共享内存 + 阻塞点管理 | 额外叠加生命周期 / 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-basics | thread-pool-basics | ThreadPoolBasics.java |
当前仍缺:Thread / Runnable / Callable / Future 专属实验(例如
run()vsstart()、submit()vsexecute()、Future.cancel(true)响应中断演示、池内互相get()饥饿复现)。
复习检查题
Thread、Runnable、Callable、Future各自扮演什么角色?答:
Thread是执行载体;Runnable是无返回值任务描述;Callable是有返回值且可抛异常的任务描述;Future是任务提交之后的结果句柄,用来等待、取消和查状态。为什么说
Future.get()是并发代码里的危险阻塞点?答:因为它会让当前线程阻塞等待结果。如果放在 Android 主线程、请求关键路径、或者小线程池内部互等路径里,就会把异步重新变成卡顿、超时或线程池饥饿问题。
cancel(true)为什么经常“看起来成功,任务却没停”?答:因为
cancel(true)只是发中断请求,不会强杀线程。任务必须自己检查中断标记,或者在可中断阻塞点上正确处理InterruptedException,才能真正停下来。什么场景下
Runnable不够用,应该换成Callable?答:当任务需要返回结果、把异常显式传回调用方、或者调用方需要用
Future做超时/取消/结果回收时,应该优先使用Callable。run()和start()的根本差异是什么?答:
run()只是普通方法调用,仍在当前线程执行;start()才会让 JVM 创建新的执行路径并异步调度run()。
速记
Thread是车,Runnable/Callable是活,Future是领结果的票。get()是阻塞点;主线程和小线程池里最危险。- 需要结果/异常/取消就上
Callable + Future;只扔任务可用Runnable。 cancel(true)依赖任务自己响应中断。