Future 与 Callable

候选人小郑在面试字节 P6 时,面试官问道:

"Callable 和 Runnable 有什么区别?Future 是什么?"

小郑说:"Callable 有返回值..."面试官追问:"Future 的 get() 是怎么阻塞的?"

小郑答不上来。面试官继续:"FutureTask 是什么?它和线程池的关系是什么?"

小郑彻底卡住了...

一、核心问题:Future 与 Callable 🔴

1.1 问题拆解

第一层:Runnable 的局限(有什么缺陷?)
  "为什么需要 Callable?Runnable 有什么问题?"
  考察点:无返回值、不能抛出受检异常

第二层:Future 阻塞机制(怎么获取结果?)
  "Future.get() 是怎么阻塞的?超时机制是怎么工作的?"
  考察点:LockSupport.park、线程等待

第三层:FutureTask 状态机(怎么工作?)
  "FutureTask 的状态转换是什么?"
  考察点:NEW/COMPLETING/NORMAL/EXCEPTIONAL/CANCELLED/INTERRUPTING

1.2 ❌ 错误示范

候选人原话 A:"Callable 和 Runnable 差不多,都是线程执行的东西。"

问题诊断:Callable 可以返回结果、可以抛出受检异常;Runnable 不能返回结果(JDK 1.4 之前),也不能抛出受检异常(run() 签名没有 throws)。

候选人原话 B:"Future.get() 是 busy-waiting(忙等待)。"

问题诊断:Future.get() 使用 LockSupport.park() 挂起线程,不消耗 CPU。这比忙等待高效得多。

1.3 标准回答

P5 级别:Callable vs Runnable

Runnable 的局限

interface Runnable {
    void run();  // 不能返回值,不能抛出受检异常
}

class MyRunnable implements Runnable {
    public void run() {
        // 无法返回计算结果
        // 无法抛出一个 IOException(受检异常)
    }
}

Callable 的设计

interface Callable<V> {
    V call() throws Exception;  // 可以返回结果,可以抛出受检异常
}

class MyCallable implements Callable<Integer> {
    public Integer call() throws IOException {
        int result = compute();
        return result;  // 返回结果
    }
}

Future 的作用

Callable 的 call() 是异步执行的,Future 用于获取异步结果:

ExecutorService executor = Executors.newFixedThreadPool(4);

Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};

Future<Integer> future = executor.submit(task);

// 阻塞等待结果
Integer result = future.get();  // 等待 1 秒,返回 42

// 非阻塞轮询
while (!future.isDone()) {
    // 做其他事
}
Integer result = future.get();

P6 级别:Future 的阻塞机制

FutureTask 的状态机

// FutureTask 的状态(volatile)
private volatile int state;
private static final int
    NEW          = 0;      // 初始状态
    COMPLETING   = 1;      // 正在设置结果
    NORMAL       = 2;      // 正常完成,结果已设置
    EXCEPTIONAL  = 3;      // 异常完成
    CANCELLED    = 4;      // 被取消
    INTERRUPTING = 5;      // 正在中断
    INTERRUPTED  = 6;      // 已被中断

状态转换图

graph TD
    A[NEW] -->|call() 正常返回| B[COMPLETING] -->|结果设置完成| C[NORMAL]
    A -->|call() 抛异常| D[COMPLETING] -->|异常设置完成| E[EXCEPTIONAL]
    A -->|cancel(false)| F[CANCELLED]
    A -->|cancel(true)| G[INTERRUPTING] -->|中断完成| H[INTERRUPTED]

get() 的阻塞实现

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING) {  // 任务未完成
        s = awaitDone(true, 0L);  // 阻塞等待
    }
    return report(s);  // 报告结果或异常
}

private int awaitDone(boolean timed, long nanos) throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    while (true) {
        if (Thread.interrupted()) {
            removeWaiter(node);
            throw new InterruptedException();
        }
        int s = state;
        if (s > COMPLETING) {  // 任务已完成
            if (node != null) node.waiter = null;
            return s;
        }
        if (timed && nanos <= 0) {
            return futureTask.cancel(false);
        }
        if (s <= COMPLETING) {  // 未完成,park
            LockSupport.parkNanos(this, nanos);
        }
    }
}

get(timeout) 的超时版本

public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
    if (Thread.interrupted()) throw new InterruptedException();
    long nanos = unit.toNanos(timeout);
    int s = state;
    if (s <= COMPLETING) {
        s = awaitDone(true, nanos);
        if (s <= COMPLETING)  // 超时,任务仍未完成
            throw new TimeoutException();
    }
    return report(s);
}

P7 级别:FutureTask 与线程池

FutureTask 的设计

FutureTask 同时实现了 Runnable 和 Future 接口:

public class FutureTask<V> implements RunnableFuture<V> {}

public interface RunnableFuture<V> extends Runnable, Future<V> {}

这使得 FutureTask 可以:

  1. 被 Thread 直接运行new Thread(new FutureTask<>(callable)).start()
  2. 被 ExecutorService.submit() 返回Future<V> future = executor.submit(callable)

线程池的 submit() 流程

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);  // 包装为 FutureTask
    execute(ftask);  // 执行
    return ftask;    // 返回 FutureTask
}

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<>(callable);  // 默认实现
}

【面试官心理】 这道题我能问到 P7 级别,是因为 FutureTask 的状态机是 JUC 中的经典设计。能说清 COMPLETING 中间状态的候选人说明他理解了这个设计的复杂性。能说出 RunnableFuture 双重身份的候选人说明他对接口设计有了解。

1.4 追问升级

追问 1:FutureTask 的 DONE 和 COMPLETING 有什么区别?

  • COMPLETING:正在设置结果(set() 正在执行中,还没设置完)
  • DONE:已完成(结果已设置或异常/取消)

COMPLETING 是一个极短的中间状态,发生在结果赋值的一瞬间。在 awaitDone() 中需要处理这个状态以避免遗漏。

追问 2:Future 的 cancel() 做了什么?

public boolean cancel(boolean mayInterruptIfRunning) {
    if (state != NEW) return false;  // 只能取消 NEW 状态的任务
    if (mayInterruptIfRunning) {
        state = INTERRUPTING;
        runner.interrupt();  // 中断执行线程
        state = INTERRUPTED;
    } else {
        state = CANCELLED;
    }
    return true;
}

如果 mayInterruptIfRunning=true,会中断正在执行的线程;如果为 false,则设置状态为 CANCELLED,但不会中断线程(任务继续运行,但 get() 会抛 CancellationException)。

二、与 CompletableFuture 的对比 🟡

2.1 Future 的局限性

// Future 的问题:无法组合多个 Future
Future<A> f1 = executor.submit(() -> fetchA());
Future<B> f2 = executor.submit(() -> fetchB());

// 等待两个都完成
while (!f1.isDone() || !f2.isDone()) { /* 轮询 */ }
A a = f1.get();
B b = f2.get();

// 这是丑陋的!需要手动轮询或阻塞

2.2 CompletableFuture 的优势

// CompletableFuture:链式组合
CompletableFuture<A> f1 = CompletableFuture.supplyAsync(() -> fetchA());
CompletableFuture<B> f2 = CompletableFuture.supplyAsync(() -> fetchB());

// 等待两个都完成
CompletableFuture<C> combined = f1.thenCombine(f2, (a, b) -> combine(a, b));
C c = combined.get();

// 更优雅的写法
CompletableFuture.supplyAsync(() -> fetchA())
    .thenCombine(CompletableFuture.supplyAsync(() -> fetchB()), (a, b) -> combine(a, b))
    .thenAccept(c -> System.out.println(c));

三、生产避坑 🟡

3.1 Future.get() 忘记超时

// 错误:无限阻塞
future.get();  // 如果任务永远不完成,线程永久阻塞

// 正确:设置超时
future.get(30, TimeUnit.SECONDS);  // 30 秒超时

3.2 异常处理

// 错误:Future.get() 可能抛出 ExecutionException
try {
    future.get();
} catch (Exception e) {
    // e.getMessage() 是 ExecutionException,需要解包
    Throwable cause = e.getCause();
}

// 正确:解包异常
try {
    future.get();
} catch (ExecutionException e) {
    throw (IOException) e.getCause();  // 还原原始异常
}
💡

面试加分点:能说出"JDK 8 的 CompletableFuture 内部使用 ForkJoinPool.commonPool(),可以通过 completableFuture.getNumberOfDependents() 查看依赖该 future 的任务数",说明他对 JDK 8 异步工具有深入了解。

⚠️

面试陷阱:被问到"FutureTask 的 runner 是什么",很多人会说"是 Callable 的返回值"。准确答案是:runner 是持有 FutureTask 并在其中执行 call() 的 Thread 对象,通过 CAS 设置(UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))。