Threading Basics

线程基础:Thread、Runnable、Callable 与 Future

Java 中"如何创建一个可执行任务"这件事,经历了从最原始到逐步解耦的演进。 本文按 Thread → Runnable → Callable → Future 的脉络,梳理它们各自解决什么问题、怎么用、以及局限在哪。

一、整体脉络

一个核心思想,贯穿四个演进阶段。

Java 中"创建一个可执行任务"这件事,经历了从 Thread → Runnable → Callable → Future 的演进:

Thread
线程本身(重量级,任务与线程耦合)
→
Runnable
任务抽象(无返回值、不能抛受检异常)
→
Callable
任务抽象(有返回值、可抛异常)
→
Future
异步结果的"凭证"(拿到未来才会有的结果)
核心思想:把"任务是什么"和"由谁执行、怎么执行"解耦。这是后来线程池、 CompletableFuture 的基础。

二、Thread

最底层、最重量的那一层。

Thread 是 JVM 中对操作系统线程的封装,是真正被 CPU 调度执行的最小单位。

用法

常见有两种使用方式:

  • 把任务交给线程: new Thread(runnable).start();
  • 继承 Thread 并重写 run(): 在子类中覆盖 run() 方法,再调用 start()。

注意

不要对同一个线程对象重复调用 start(),否则会抛出 IllegalThreadStateException。

三、Runnable

把"任务"从"线程"里拆出来的第一步。

Runnable 是一个函数式接口,实现它即可定义多线程任务。记得重写其中的 run() 方法:

@FunctionalInterface
public interface Runnable {
    void run();   // 无返回值,不能抛受检异常
}Java

1. 使用方法

// 方式一:作为任务传给 Thread
Runnable r = () -> System.out.println("hello");
new Thread(r).start();

// 方式二:提交给线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.execute(r);Java

2. 为什么使用 Runnable

  • 解耦: 任务逻辑独立于线程,可复用、可测试。
  • 避免单继承限制: Java 单继承,继承 Thread 就不能再继承别的类;实现接口则没有这个限制。
  • 线程池友好: 它是线程池 execute() 的参数类型。

3. 局限

  • 没有返回值 → 想知道任务算出了什么,只能靠共享变量 + 同步,很别扭。
  • 不能抛受检异常 → 只能自己 try-catch 包起来。
  • 无法知道任务是否完成、是否失败。

这两点直接催生了 Callable / Future。

四、Callable

"带返回值的 Runnable"。

1. 是什么

Callable 是带返回值的任务接口,与 Runnable 类似,但支持返回结果并抛出异常。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;   // 有返回值,可抛异常
}Java

2. 用法

Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(task);   // 注意用 submit 而不是 execute
Integer result = future.get();  // 阻塞直到拿到结果Java

3. 何时用

  • 任务需要返回结果(计算、查询、远程调用)。
  • 任务可能抛异常,希望把异常传递给调用方。
  • 需要配合 Future 做异步编排。

4. 注意

  • Callable 只能提交给线程池(submit),不能直接 new Thread(callable) —— Thread 构造只接受 Runnable。
  • 如果想用 Thread 跑 Callable,需要 FutureTask(见下节)。

五、Future

异步结果的"凭证"。

1. 是什么

Future 是异步计算结果的句柄(占位符)。提交任务后立即返回,未来某个时刻可以通过它拿结果。

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, TimeoutException, ExecutionException;
}Java

2. 核心方法

方法作用
get()阻塞获取结果;任务未完成就一直等
get(timeout, unit)带超时的等待,超时抛 TimeoutException
isDone()任务是否已结束(正常 / 异常 / 取消都算 done)
cancel(true)尝试取消;true 表示中断正在执行的线程
isCancelled()是否被取消

3. 异常处理

  • 任务内部抛异常 → 调用 get() 时抛 ExecutionException,用 getCause() 拿原始异常。
  • 不调用 get(),异常会被静默吞掉——这是常见坑。

4. FutureTask

FutureTask 同时实现 Runnable 和 Future,是"可执行 + 可拿结果"的桥梁:

FutureTask<Integer> ft = new FutureTask<>(() -> 1 + 1);
new Thread(ft).start();      // 既能当 Runnable
Integer r = ft.get();        // 又能当 FutureJava

线程池 submit(Callable) 内部其实就是把 Callable 包成 FutureTask 再执行。

5. 局限(为什么后来有 CompletableFuture)

  • get() 是阻塞的,无法"完成后自动回调"。
  • 无法方便地组合多个 Future(链式、合并、任一完成)。
  • 没有 thenApply / thenCompose / allOf 这类编排能力。
→ 这些由 CompletableFuture 解决。