Callable、FutureTask 与有返回值任务 | JavaSE
Callable、FutureTask 与有返回值任务
一、学习目标
完成本章后,你应该能够:
- 理解 Runnable 为什么不适合直接表达“有返回值任务”。
- 理解
Callable<V>的作用以及泛型参数V的含义。 - 独立实现
Callable<V>并重写call()。 - 理解
call()与Runnable.run()的核心区别。 - 理解
Future所代表的“未来结果”模型。 - 理解
FutureTask<V>为什么能够连接 Callable 与 Thread。 - 独立完成
Callable → FutureTask → Thread → start → get完整流程。 - 理解
FutureTask.get()为什么可能阻塞当前线程。 - 理解任务执行异常如何通过
ExecutionException传递给获取结果的一方。 - 能够比较 Thread、Runnable、Callable 三种基本任务创建模型。
二、核心知识
2.1 Runnable 遇到了什么问题
上一章:
Runnable task = () -> {
System.out.println("执行任务");
};
非常适合:
执行一件事情
但是 Runnable 的核心方法:
void run();
有两个重要限制。
第一个:
没有返回值
第二个:
方法签名不能直接声明 throws Exception
例如现在有一个需求:
创建一个子线程计算
1~100的总和,然后把结果交回 main 线程。
如果使用 Runnable:
@Override
public void run() {
long sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
// 怎么规范地把 sum 返回给调用方?
}
你不能写:
return sum;
因为:
run()
是:
void
于是 Java 并发包提供了:
Callable<V>
用于描述:
可以返回结果的任务。
2.2 Callable 是什么
Callable 位于:
java.util.concurrent
其核心结构可以理解为:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
这里最重要的三个部分:
Callable<V>
V call()
throws Exception
2.3 泛型参数 V 表示什么
例如:
Callable<String>
表示:
这个任务最终返回 String
那么:
@Override
public String call() throws Exception {
}
如果:
Callable<Integer>
那么:
@Override
public Integer call() throws Exception {
}
如果:
Callable<Long>
则:
@Override
public Long call() throws Exception {
}
因此:
Callable<V>
中的:
V
就是:
call()最终返回结果的数据类型。
2.4 Callable 与 Runnable 的核心区别
| 对比 | Runnable | Callable |
| -------- | -------------- | ------------------------------ |
| 核心方法 | run() | call() |
| 返回值 | void | V |
| 泛型 | 无返回结果泛型 | <V> 表示结果类型 |
| 受检异常 | run() 不声明 | call() 可 throws Exception |
| 典型用途 | 执行任务 | 执行并获得结果 |
例如:
Runnable task = () -> {
System.out.println("保存日志");
};
适合:
做完即可
而:
Callable<Integer> task = () -> {
return 100;
};
适合:
执行以后还要结果
2.5 第一个 Callable
定义:
import java.util.concurrent.Callable;
class SumTask implements Callable<Long> {
@Override
public Long call() throws Exception {
long sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
现在:
SumTask
代表一个:
最终可以得到 Long 结果的任务
但是问题来了。
我们不能直接写:
new Thread(new SumTask());
为什么?
因为 Thread 常见构造器接收的是:
Runnable
而:
SumTask
实现的是:
Callable<Long>
二者不是直接的父子接口关系。
所以需要一个中间桥梁:
FutureTask
2.6 Future 是什么
假设:
子线程正在计算
结果可能需要:
2秒以后
才产生。
main 线程现在想拿结果,但结果还不存在。
我们需要一个对象代表:
这个现在还没有、未来可能得到的计算结果。
这就是:
Future<V>
可以把 Future 理解成:
未来结果的凭证
例如:
任务正在执行
↓
Future<Long>
↓
未来可以取得 Long
它不仅可以表示:
结果
还可以查询:
任务是否完成
任务是否取消
以及尝试:
取消任务
2.7 FutureTask 是什么
FutureTask<V> 是:
java.util.concurrent.FutureTask
它非常关键。
从类型关系上看,可以建立:
FutureTask<V>
│
└── 实现 RunnableFuture<V>
│
├── Runnable
└── Future<V>
于是 FutureTask 同时具有两种身份:
Runnable
+
Future
这意味着它既能:
作为 Runnable 被 Thread 执行
又能:
保存并提供未来任务结果
这就是它为什么能够连接:
Callable
和:
Thread
2.8 Callable → FutureTask → Thread
完整关系:
Callable<V>
│
│ 描述有返回值任务
▼
FutureTask<V>
│
├── Runnable 身份
│ ↓
│ Thread
│
└── Future 身份
↓
get()
↓
获取结果
因此:
Callable
解决:任务是什么、返回什么
FutureTask
解决:包装任务 + 保存未来结果
Thread
解决:创建真正的执行线程
三、使用方法
3.1 第一步:实现 Callable
import java.util.concurrent.Callable;
class SumTask implements Callable<Long> {
private final int n;
public SumTask(int n) {
this.n = n;
}
@Override
public Long call() throws Exception {
long sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
}
这里:
Callable<Long>
表示:
任务最终返回 Long
3.2 第二步:创建 Callable 对象
Callable<Long> callable = new SumTask(100);
此时只有:
任务对象
还没有线程开始执行。
3.3 第三步:封装成 FutureTask
FutureTask<Long> futureTask =
new FutureTask<>(callable);
FutureTask 的泛型:
<Long>
与 Callable 返回结果:
Long
对应。
现在:
futureTask
既包含:
Callable 任务
又可以保存:
任务未来产生的结果
3.4 第四步:交给 Thread
因为 FutureTask 实现了 Runnable,所以可以:
Thread thread = new Thread(futureTask);
注意:
Thread 并不直接认识 Callable
真正的桥梁是:
FutureTask
3.5 第五步:启动线程
thread.start();
此时:
新线程
↓
执行 FutureTask
↓
FutureTask 内部执行 Callable.call()
最终:
return sum;
产生结果。
3.6 第六步:获取结果
调用:
Long result = futureTask.get();
完整程序:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableDemo {
public static void main(String[] args) {
Callable<Long> callable = new SumTask(100);
FutureTask<Long> futureTask =
new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
Long result = futureTask.get();
System.out.println(
"1~100 的和:" + result
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.out.println(
"任务执行失败:" + e.getCause()
);
}
}
}
class SumTask implements Callable<Long> {
private final int n;
public SumTask(int n) {
this.n = n;
}
@Override
public Long call() throws Exception {
long sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
}
整体流程:
main
│
├── new SumTask()
│
├── new FutureTask<>(callable)
│
├── new Thread(futureTask)
│
├── thread.start()
│
│
│ 子线程
│ │
│ ▼
│ FutureTask.run()
│ │
│ ▼
│ callable.call()
│ │
│ ▼
│ return result
│
└── futureTask.get()
│
▼
得到结果
3.7 使用 Lambda 创建 Callable
Callable 也是函数式接口。
因此:
Callable<Integer> task = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 100;
}
};
可以写成:
Callable<Integer> task = () -> {
return 100;
};
进一步:
Callable<Integer> task = () -> 100;
完整:
Callable<Integer> callable = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
};
FutureTask<Integer> futureTask =
new FutureTask<>(callable);
new Thread(futureTask).start();
然后:
Integer result = futureTask.get();
四、原理与进阶
4.1 FutureTask 为什么能够放入 Thread
Thread 的构造器需要:
Runnable
FutureTask 的类型关系是:
FutureTask<V>
↓
RunnableFuture<V>
↓
Runnable + Future<V>
所以:
FutureTask<Long> futureTask =
new FutureTask<>(callable);
当然可以:
new Thread(futureTask);
这是整个 Callable 线程模型最容易卡住的地方。
一定要记:
Thread 不直接接收 Callable
Callable
↓
FutureTask 包装
↓
FutureTask 是 Runnable
↓
交给 Thread
4.2 get() 为什么可能阻塞
假设:
thread.start();
以后任务需要:
5 秒
才能完成。
紧接着 main 调用:
futureTask.get();
但此时:
结果还不存在
怎么办?
get() 会等待。
也就是:
main线程
│
├── start()
│
└── get()
│
│ 结果没有完成
▼
等待
│
│
子线程继续执行任务
│
▼
任务完成
│
▼
产生结果
│
▼
main线程获得结果并继续
所以:
Future.get()不是“永远立即拿到结果”。
如果任务还没有完成:
调用 get() 的线程会等待
这就是阻塞。
4.3 不要过早调用 get()
例如:
thread.start();
String result = futureTask.get();
System.out.println("处理其他工作");
如果 Callable 很耗时,那么:
main
刚启动任务就立即:
get()
可能马上进入等待。
这样并发价值会降低。
有时候更合理:
thread.start();
System.out.println("main先处理其他任务");
// 其他工作……
String result = futureTask.get();
模型变成:
子线程:计算任务
↓
同时执行
main:处理其他事情
↓
需要结果时
↓
get()
这才更符合:
异步计算
的思维。
4.4 get() 与异常传播
Callable 的:
call()
可以:
throws Exception
例如:
@Override
public Integer call() throws Exception {
throw new Exception("计算失败");
}
那么执行任务的子线程发生异常后,结果获取方在:
futureTask.get()
时会通过:
ExecutionException
观察到任务失败。
例如:
try {
Integer result = futureTask.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
System.out.println(
"真正的任务异常:" + cause
);
}
因此:
Callable.call()
↓
任务执行异常
↓
FutureTask 记录异常
↓
get()
↓
ExecutionException
这是 Callable 相比 Runnable 更适合:
有结果计算任务
的重要原因之一。
4.5 InterruptedException 是谁被中断
看:
futureTask.get();
如果结果尚未完成:
当前线程正在等待
假设当前调用 get() 的是:
main线程
如果 main 在等待过程中被中断:
get()
会抛出:
InterruptedException
所以这里的:
InterruptedException
关注的是:
等待结果的当前线程被中断。
初学阶段常见处理:
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
恢复中断标记,把中断信息继续保留下来。
关于线程中断机制,本课程不在这里大规模展开。
4.6 FutureTask 不等于 Thread
必须避免一个概念错误:
FutureTask<Integer> futureTask
不是:
线程
它仍然是:
任务 + 未来结果容器
真正执行它的可以是:
Thread
例如:
new Thread(futureTask).start();
以后也可以是:
线程池
线程池章节会看到:
ExecutorService
直接管理并执行 Callable,而开发者通常不会再手动:
new Thread(...)
因此 Future/Callable 的价值并不局限于这一章。
4.7 FutureTask 还可以包装 Runnable
FutureTask 不只能包装:
Callable
官方还提供:
FutureTask(Runnable runnable, V result)
例如:
Runnable task = () -> {
System.out.println("任务执行完成");
};
FutureTask<String> futureTask =
new FutureTask<>(task, "success");
new Thread(futureTask).start();
任务成功完成后:
futureTask.get()
可以得到预先指定的:
"success"
不过当前章节的学习主线仍然是:
Callable
+
FutureTask
因为它最直接体现:
有返回值任务
这一核心能力。
五、实践应用
5.1 后台计算结果
例如统计订单金额:
Callable<Long> task = () -> {
long total = 0;
for (int price : new int[]{100, 200, 300}) {
total += price;
}
return total;
};
如果计算复杂,可以交给其他线程:
FutureTask<Long> future =
new FutureTask<>(task);
new Thread(future).start();
main 同时继续:
处理界面
处理其他请求
准备其他数据
最后:
Long total = future.get();
取得结果。
5.2 文件统计任务
例如:
统计某个目录中文件数量
可能需要较长时间。
任务最终需要返回:
文件数量
因此:
Callable<Integer>
比:
Runnable
更加自然。
5.3 多个独立计算任务
假设需要分别计算:
1~100
1~1000
1~10000
可以:
FutureTask<Long> f1 =
new FutureTask<>(new SumTask(100));
FutureTask<Long> f2 =
new FutureTask<>(new SumTask(1000));
FutureTask<Long> f3 =
new FutureTask<>(new SumTask(10000));
new Thread(f1).start();
new Thread(f2).start();
new Thread(f3).start();
之后:
long result1 = f1.get();
long result2 = f2.get();
long result3 = f3.get();
这里已经开始接近:
批量提交任务
统一管理任务
统一收集结果
的需求。
继续发展下去,自然就会进入:
线程池
ExecutorService
Future
体系。
六、常见问题
6.1 Callable 是线程吗?
不是。
Callable 表示:
有返回值的任务
真正的线程仍然是:
Thread
6.2 为什么 Callable 不能直接放进 Thread?
因为 Thread 常见任务构造器接收:
Runnable
Callable 并没有继承 Runnable。
需要:
Callable
↓
FutureTask
↓
Runnable
↓
Thread
6.3 FutureTask 是线程吗?
不是。
FutureTask 是:
可执行任务
+
未来结果
的组合对象。
6.4 为什么 FutureTask 可以传给 Thread?
因为 FutureTask 实现的:
RunnableFuture
本身继承:
Runnable
所以 FutureTask 同时具有 Runnable 身份。
6.5 get() 一定立即返回吗?
不一定。
如果任务没有完成:
get()
会等待任务完成。
6.6 Callable 一定要写 throws Exception 吗?
实现方法可以声明:
throws Exception
但如果任务内部完全不需要抛异常,也可以省略。
例如:
@Override
public Integer call() {
return 100;
}
合法。
6.7 Callable 能不能使用 Lambda?
可以。
Callable 是函数式接口。
例如:
Callable<Integer> task = () -> 100;
6.8 ExecutionException 和真正异常是一回事吗?
不是完全一回事。
如果任务执行失败:
真正业务异常
↓
被 Future 封装
↓
get()
↓
ExecutionException
通常可以通过:
e.getCause()
查看真正导致任务失败的异常。
6.9 Callable 比 Runnable 一定更好吗?
不是。
如果任务只是:
执行某个操作
并不需要结果:
Runnable
更加简单。
如果任务:
需要返回值
或者希望通过 Future 获取执行结果
Callable 更合适。
技术选择应该根据需求,而不是“越复杂越高级”。
七、练习与验收
7.1 知识问答
- Runnable 最大的结果限制是什么?
- Callable 位于哪个包?
- Callable 的核心方法是什么?
Callable<V>中 V 表示什么?call()与run()的返回类型有什么区别?- Callable 为什么更适合计算任务?
- Callable 能否声明抛出异常?
- Future 表示什么?
- 为什么把 Future 理解为“未来结果凭证”比较合理?
- FutureTask 是什么?
- FutureTask 为什么可以交给 Thread?
- Callable 为什么不能直接交给 Thread?
- 完整的 Callable 创建线程流程是什么?
FutureTask.get()有什么作用?- 为什么
get()可能阻塞? - 如果 Callable 执行时抛出异常,获取结果时可能出现什么异常?
ExecutionException.getCause()有什么意义?- Callable 能否使用 Lambda?
- FutureTask 是否等于 Thread?
- Runnable 与 Callable 应该如何选择?
7.2 代码阅读
不要运行:
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
};
FutureTask<Integer> future =
new FutureTask<>(task);
Thread thread = new Thread(future);
thread.start();
System.out.println(future.get());
回答:
task是线程还是任务?- Callable 的返回类型是什么?
- FutureTask 的泛型为什么也是 Integer?
- 为什么 FutureTask 可以交给 Thread?
- 最终结果是多少?
get()由哪条线程调用?- 如果执行到
get()时任务还没完成,会发生什么?
阅读:
Callable<String> task = () -> {
throw new Exception("任务失败");
};
FutureTask<String> future =
new FutureTask<>(task);
new Thread(future).start();
String result = future.get();
回答:
call()是否允许抛出这个异常?- 子线程任务能否正常产生 String 结果?
- 调用
get()时会发生什么? - 如何查看真正导致失败的异常?
7.3 手写代码
任务一:计算 1~100
从零实现:
SumTask implements Callable<Long>
要求:
计算 1~100 的和
返回 Long
然后完成:
Callable
→ FutureTask
→ Thread
→ start
→ get
完整流程。
任务二:可配置 n
改造:
new SumTask(100);
new SumTask(1000);
new SumTask(10000);
分别启动三个任务,并获取三个结果。
任务三:Lambda
不创建 Callable 实现类。
直接使用:
Callable<Integer> task = () -> {
...
};
计算:
1~1000 中所有偶数的和
7.4 Debug
分析:
class MyTask implements Callable<Integer> {
@Override
public void run() {
return 100;
}
}
指出所有问题。
至少考虑:
方法名称
返回类型
return
继续分析:
Callable<Integer> task = () -> 100;
Thread thread = new Thread(task);
thread.start();
判断为什么不能直接这样写。
要求画出正确:
Callable
FutureTask
Thread
三者关系。
继续分析:
FutureTask<Integer> future =
new FutureTask<>(() -> 100);
System.out.println(future.get());
new Thread(future).start();
回答:
get()调用时任务是否已经开始执行?- 程序可能出现什么问题?
- 为什么正确顺序应该先启动任务?
7.5 综合训练
设计“并发成绩统计器”。
现在存在三个计算任务:
任务 A:计算 Java 平均分
任务 B:计算数学平均分
任务 C:计算英语平均分
每个任务最终返回:
Double
要求:
- 每个任务使用 Callable;
- 每个任务分别使用 FutureTask;
- 分别交给三个 Thread;
- 三个线程全部启动;
- main 在任务执行期间输出:
正在统计成绩……
- 最后获取三个结果;
- 输出:
Java平均分:
数学平均分:
英语平均分:
完成后回答:
- 为什么这里使用 Callable 比 Runnable 更自然?
- 为什么应该先启动三个线程,再依次调用 get?
- FutureTask 在整个设计中承担了什么角色?
- 如果其中一个任务发生异常,应该在哪里观察?
7.6 本章验收
如果能够闭卷完成以下任务,可以认为本章基本掌握:
- 默写
Callable<V>的核心方法。 - 解释泛型参数 V。
- 说出 Runnable 与 Callable 两个核心区别。
- 手写一个返回 Integer 的 Callable。
- 默写:
Callable
→ FutureTask
→ Thread
→ start()
→ get()
完整链路。
- 解释 FutureTask 为什么可以传入 Thread。
- 解释
get()为什么可能阻塞。 - 解释任务异常与
ExecutionException的关系。 - 使用 Lambda 创建 Callable。
- 能够根据“是否需要任务返回结果”选择 Runnable 或 Callable。