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 知识问答

  1. Runnable 最大的结果限制是什么?
  2. Callable 位于哪个包?
  3. Callable 的核心方法是什么?
  4. Callable<V> 中 V 表示什么?
  5. call()run() 的返回类型有什么区别?
  6. Callable 为什么更适合计算任务?
  7. Callable 能否声明抛出异常?
  8. Future 表示什么?
  9. 为什么把 Future 理解为“未来结果凭证”比较合理?
  10. FutureTask 是什么?
  11. FutureTask 为什么可以交给 Thread?
  12. Callable 为什么不能直接交给 Thread?
  13. 完整的 Callable 创建线程流程是什么?
  14. FutureTask.get() 有什么作用?
  15. 为什么 get() 可能阻塞?
  16. 如果 Callable 执行时抛出异常,获取结果时可能出现什么异常?
  17. ExecutionException.getCause() 有什么意义?
  18. Callable 能否使用 Lambda?
  19. FutureTask 是否等于 Thread?
  20. 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());

回答:

  1. task 是线程还是任务?
  2. Callable 的返回类型是什么?
  3. FutureTask 的泛型为什么也是 Integer?
  4. 为什么 FutureTask 可以交给 Thread?
  5. 最终结果是多少?
  6. get() 由哪条线程调用?
  7. 如果执行到 get() 时任务还没完成,会发生什么?

阅读:

Callable<String> task = () -> {
    throw new Exception("任务失败");
};

FutureTask<String> future =
        new FutureTask<>(task);

new Thread(future).start();

String result = future.get();

回答:

  1. call() 是否允许抛出这个异常?
  2. 子线程任务能否正常产生 String 结果?
  3. 调用 get() 时会发生什么?
  4. 如何查看真正导致失败的异常?

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();

回答:

  1. get() 调用时任务是否已经开始执行?
  2. 程序可能出现什么问题?
  3. 为什么正确顺序应该先启动任务?

7.5 综合训练

设计“并发成绩统计器”。

现在存在三个计算任务:

任务 A:计算 Java 平均分
任务 B:计算数学平均分
任务 C:计算英语平均分

每个任务最终返回:

Double

要求:

  • 每个任务使用 Callable;
  • 每个任务分别使用 FutureTask;
  • 分别交给三个 Thread;
  • 三个线程全部启动;
  • main 在任务执行期间输出:
正在统计成绩……
  • 最后获取三个结果;
  • 输出:
Java平均分:
数学平均分:
英语平均分:

完成后回答:

  1. 为什么这里使用 Callable 比 Runnable 更自然?
  2. 为什么应该先启动三个线程,再依次调用 get?
  3. FutureTask 在整个设计中承担了什么角色?
  4. 如果其中一个任务发生异常,应该在哪里观察?

7.6 本章验收

如果能够闭卷完成以下任务,可以认为本章基本掌握:

  • 默写 Callable<V> 的核心方法。
  • 解释泛型参数 V。
  • 说出 Runnable 与 Callable 两个核心区别。
  • 手写一个返回 Integer 的 Callable。
  • 默写:
Callable
→ FutureTask
→ Thread
→ start()
→ get()

完整链路。

  • 解释 FutureTask 为什么可以传入 Thread。
  • 解释 get() 为什么可能阻塞。
  • 解释任务异常与 ExecutionException 的关系。
  • 使用 Lambda 创建 Callable。
  • 能够根据“是否需要任务返回结果”选择 Runnable 或 Callable。