深入解析FutureTask:原理与实战

我们来深入解析 FutureTask。下面将从它们的用法开始,逐步深入到底层实现、方法和接口设计,并探讨它们对于我们自己设计实现以及其他编程实践的学习意义。

主要就是放入等待队列(CAS操作一个链表头),完成任务的线程唤醒等待线程继续逻辑。

FutureTask 是一个可取消的异步计算任务。它实现了 RunnableFuture 接口,该接口同时继承了 Runnable 和 Future 接口。这意味着 FutureTask 既可以作为一个 Runnable 被执行器(如 ExecutorService)执行,也可以作为一个 Future 来获取异步计算的结果、查询计算状态或取消计算。

FutureTask 通常用于包装一个 Callable 或 Runnable 对象。

基本用法示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class FutureTaskExample {
    public static void main(String[] args) throws Exception {
        // 1. 创建 Callable 任务
        Callable<String> callableTask = () -> {
            System.out.println(Thread.currentThread().getName() + " is executing callable task...");
            TimeUnit.SECONDS.sleep(2); // 模拟耗时操作
            return "Callable Result";
        };

        // 2. 使用 Callable 创建 FutureTask
        FutureTask<String> futureTask1 = new FutureTask<>(callableTask);

        // 也可以使用 Runnable 创建 FutureTask (通常需要提供一个结果,若无则为 null)
        Runnable runnableTask = () -> {
            System.out.println(Thread.currentThread().getName() + " is executing runnable task...");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };
        FutureTask<String> futureTask2 = new FutureTask<>(runnableTask, "Runnable Result (if provided)");


        // 3. 提交 FutureTask 到 ExecutorService 执行
        // FutureTask 本身就是 Runnable,可以直接被线程执行或提交给 ExecutorService
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(futureTask1);
        executor.submit(futureTask2);
        // 或者直接 new Thread(futureTask1).start();

        System.out.println("Tasks submitted.");

        // 4. 获取结果 (get() 方法会阻塞直到任务完成)
        try {
            System.out.println("Waiting for futureTask1 result...");
            String result1 = futureTask1.get(); // 阻塞等待
            System.out.println("futureTask1 result: " + result1);

            System.out.println("Waiting for futureTask2 result with timeout...");
            // get(long timeout, TimeUnit unit) 可以设置超时
            String result2 = futureTask2.get(2, TimeUnit.SECONDS);
            System.out.println("futureTask2 result: " + result2);

        } catch (InterruptedException e) {
            System.err.println("Task interrupted: " + e.getMessage());
        } catch (java.util.concurrent.ExecutionException e) {
            System.err.println("Task execution failed: " + e.getCause());
        } catch (java.util.concurrent.TimeoutException e) {
            System.err.println("Task timed out: " + e.getMessage());
        }

        // 5. 检查任务状态和取消
        if (!futureTask1.isDone()) {
            System.out.println("futureTask1 is not done yet.");
        }
        if (futureTask1.isCancelled()) {
            System.out.println("futureTask1 was cancelled.");
        }

        // 尝试取消一个未完成的任务
        FutureTask<Integer> cancellableTask = new FutureTask<>(() -> {
            TimeUnit.SECONDS.sleep(5);
            return 100;
        });
        new Thread(cancellableTask).start();
        Thread.sleep(100); // 给任务一点时间启动
        boolean cancelled = cancellableTask.cancel(true); // true 表示如果任务正在运行,则中断它
        System.out.println("CancellableTask cancelled: " + cancelled);
        System.out.println("CancellableTask isCancelled: " + cancellableTask.isCancelled());
        System.out.println("CancellableTask isDone: " + cancellableTask.isDone()); // cancel 后 isDone() 也为 true

        executor.shutdown();
    }
}

FutureTask 是 Java 并发包中一个非常核心的类,它代表一个可取消的异步计算。它巧妙地结合了 Future 接口(用于获取异步计算的结果)和 Runnable 接口(使得它可以被 Executor 执行)。

状态管理 (State Management)

FutureTask 内部维护一个 volatile int state 字段来表示任务的当前状态。状态包括:

  • NEW: 初始状态,任务尚未开始或正在运行。

  • COMPLETING: 任务已完成,正在设置结果(一个短暂的中间状态)。

  • NORMAL: 任务正常完成,结果已设置。

  • EXCEPTIONAL: 任务因抛出异常而完成,异常已设置。

  • CANCELLED: 任务被取消(在开始运行前)。

  • INTERRUPTING: 任务被取消,并且正在尝试中断运行任务的线程(一个短暂的中间状态)。

  • INTERRUPTED: 任务被取消,并且运行任务的线程已被中断。

状态之间的转换通过 CAS (Compare-And-Set) 操作(使用 VarHandle STATE)来保证原子性。

为什么需要COMPLETING(set分析)

    protected void set(V v) {
        if (STATE.compareAndSet(this, NEW, COMPLETING)) {
            outcome = v;
            STATE.setRelease(this, NORMAL); // final state
            finishCompletion();
        }
    }

之所以需要 COMPLETING 这个中间状态,是为了解决一个非常棘手且经典的并发问题:丢失的唤醒(Lost Wakeup)。

如果没有 COMPLETING 状态,get() 和 set() 方法之间会存在一个致命的竞态条件。

场景一:没有 COMPLETING 状态的(错误)世界

我们假设 set 方法是这样实现的:

// 这是一个错误的、假设性的实现
protected void set(V v) {
    // 1. 先设置结果
    outcome = v;
    // 2. 再把状态从 NEW 直接改成 NORMAL
    if (STATE.compareAndSet(this, NEW, NORMAL)) {
        // 3. 最后唤醒等待者
        finishCompletion();
    }
}

现在,想象一下两个线程的执行时序:

  • 线程 T1:执行任务的线程,它刚刚计算出结果,准备调用 set(result)
  • 线程 T2:调用 get() 方法来获取结果的线程。

现在,看这个“完美风暴”般的时序:

  1. T2 (get): 调用 get(),检查 state,发现是 NEW。它判断任务还没完成,于是准备将自己加入到 waiters 等待队列中,然后挂起(park)。
  2. T1 (set): 在 T2 准备挂起但还没来得及挂起的时候,T1 抢到了 CPU 时间片。
  3. T1 (set): T1 飞快地执行完了整个 set 方法:
    • 设置 outcome = result
    • CAS 操作成功,将 state 从 NEW 改为 NORMAL
    • 调用 finishCompletion()finishCompletion 检查 waiters 队列,发现是 null(因为 T2 还没来得及把自己加进去),所以它什么也不做就返回了。
  4. T2 (get): 现在 T2 终于又获得了 CPU 时间。它继续执行第一步的计划:把自己加入 waiters 队列,然后调用 LockSupport.park() 挂起自己,等待被唤醒。

灾难发生了:T2 现在永久地挂起了。唤醒它的信号(unpark)在它睡觉之前就已经错过了。这就是“丢失的唤醒”。

场景二:COMPLETING 状态如何拯救世界

现在我们回到真实的 set 方法实现:

// ... existing code ...
protected void set(V v) {
    if (STATE.compareAndSet(this, NEW, COMPLETING)) {
        outcome = v;
        STATE.setRelease(this, NORMAL); // final state
        finishCompletion();
    }
}
// ... existing code ...

COMPLETING 状态就像一个红绿灯,它在 NEW(路口绿灯)和 NORMAL(车辆已通过)之间增加了一个 COMPLETING(黄灯,即将变红)的状态。

我们再来看一下 get() 方法内部 awaitDone() 的逻辑:

// awaitDone() 内部的循环
for (;;) {
    int s = state;
    if (s > COMPLETING) { // 如果是 NORMAL, EXCEPTIONAL 等最终状态
        // ... 直接返回结果
        return s;
    }
    else if (s == COMPLETING) // <<< 关键点在这里!
        // 任务正在完成中,短暂地让出CPU,而不是挂起
        Thread.yield();
    else if (Thread.interrupted()) {
        // ... 处理中断
    }
    else if (q == null) {
        // ... 创建 WaitNode
    }
    else if (!queued) {
        // ... 入队
    }
    else {
        // ... 挂起线程
        LockSupport.park(this);
    }
}

现在,我们用正确的逻辑重演刚才的“完美风暴”:

  1. T2 (get): 调用 get(),检查 state,发现是 NEW
  2. T1 (set): 抢到 CPU,执行 STATE.compareAndSet(this, NEW, COMPLETING)。这个操作成功了!现在 state 是 COMPLETING
  3. T2 (get): 再次获得 CPU,它在循环里重新检查 state,发现 state 变成了 COMPLETING
  4. T2 (get): 命中了 else if (s == COMPLETING) 这个分支。它不会去挂起自己,而是调用 Thread.yield(),表示“我知道结果马上就好了,我稍微等一下,让出 CPU,下一轮再来看看”。
  5. T1 (set): 继续执行,设置 outcome,然后将 state 设置为 NORMAL,最后调用 finishCompletion()
  6. T2 (get): 在下一次循环中,T2 检查 state,发现已经是 NORMAL (s > COMPLETING),于是它直接读取 outcome 并正常返回。

危机解除COMPLETING 状态在这里起到了一个至关重要的屏障作用。它告诉其他正在调用 get() 的线程:“别睡!结果马上就好,稍微自旋(spin-wait)一下!”。通过这种方式,它完美地避免了在设置结果和唤醒等待者之间的微小时间窗口内发生“丢失的唤醒”问题。

所以,COMPLETING 是一个短暂但必需的中间状态,它的存在是为了确保 FutureTask 在高并发下的绝对健壮性。

任务执行 (run() 方法)

FutureTaskrun() 方法被调用时(通常由一个 Executor 的工作线程调用):

  1. 首先会通过 CAS 操作尝试将 runner 字段(volatile Thread runner)从 null 设置为当前线程。这确保了只有一个线程可以实际执行任务。

  2. 如果设置成功并且任务状态是 NEW,则会调用内部的 Callable 对象的 call() 方法。

  3. 如果 call() 方法正常返回,则调用 set(V result) 方法设置结果,并将状态转换为 NORMAL

  4. 如果 call() 方法抛出异常,则调用 setException(Throwable t) 方法设置异常,并将状态转换为 EXCEPTIONAL

  5. finally 块中,runner 字段会被重置为 null。还会检查任务是否在运行期间被取消并需要中断(状态为 INTERRUPTINGINTERRUPTED),如果是,则会调用 handlePossibleCancellationInterrupt() 处理。

我们来逐行解析 run() 方法的实现逻辑。

// ... existing code ...
public void run() {
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}
// ... existing code ...
1. 执行前提检查与所有权确立
if (state != NEW ||
    !RUNNER.compareAndSet(this, null, Thread.currentThread()))
    return;

这是 run() 方法的入口守卫,它做了两件至关重要的事情:

  • 状态检查 (state != NEW)FutureTask 内部有一个状态机,state 字段表示当前任务的状态(NEWCOMPLETINGNORMALEXCEPTIONALCANCELLEDINTERRUPTINGINTERRUPTED)。run() 方法只能在任务处于 NEW(新建)状态时被执行。如果任务已经被执行、被取消或者正在执行,这个条件会失败,run() 方法直接返回,从而保证了任务最多只会被执行一次
  • 设置执行线程 (RUNNER.compareAndSet(...))runner 是一个 volatile 字段,用于记录当前正在执行此任务的线程。这里使用 compareAndSet (CAS) 原子操作来尝试将 runner 字段从 null 设置为 Thread.currentThread()(当前线程)。
    • 为什么需要 CAS? 这解决了并发执行 run() 的问题。假设两个线程同时调用同一个 FutureTask 实例的 run() 方法。如果没有 CAS,两个线程可能都会通过 state == NEW 的检查。但有了 CAS,只有一个线程能够成功地将 runner 从 null 设置为自己,从而“赢得”执行权。另一个线程的 CAS 操作会失败,导致整个 if 条件为 true,然后 return,避免了任务被重复执行。

这个入口守卫是确保 FutureTask 线程安全和“一次性”执行语义的关键。

2. 核心任务执行
try {
    Callable<V> c = callable;
    if (c != null && state == NEW) {
        V result;
        boolean ran;
        try {
            result = c.call();
            ran = true;
        } catch (Throwable ex) {
            result = null;
            ran = false;
            setException(ex);
        }
        if (ran)
            set(result);
    }
} 
// ...

这部分是实际执行任务的逻辑:

  • 获取 CallableFutureTask 在构造时会接收一个 Callable 或 Runnable 对象,内部统一包装成 Callable。这里先获取到这个 callable
  • 再次检查状态: 在 try 块内部,再次检查 state == NEW。这是一个双重检查,虽然入口处已经检查过一次,但在 run() 方法执行期间,任务可能被其他线程取消 (cancel())。如果在 run() 方法已经开始但 c.call() 尚未执行时任务被取消,state 会改变。这次检查确保我们不会去执行一个已经被取消的任务。
  • 执行 call(): 调用 callable 的 call() 方法,这是任务的业务逻辑所在。
    • 成功: 如果 call() 正常返回,ran 标志被设为 true,结果保存在 result 变量中。
    • 异常: 如果 call() 抛出任何 Throwable(包括 Error 和 Exception),它会被捕获。ran 设为 false,然后调用 setException(ex) 来保存异常信息,并将任务状态设置为 EXCEPTIONAL
  • 设置结果: 如果任务成功执行 (ran 为 true),则调用 set(result) 来保存正常执行的结果,并将任务状态设置为 NORMAL

set() 和 setException() 这两个方法不仅会设置结果/异常和更新状态,还会负责唤醒所有正在调用 get() 方法等待结果的线程。

3. finally 块中的清理工作
finally {
    // runner must be non-null until state is settled to
    // prevent concurrent calls to run()
    runner = null;
    // state must be re-read after nulling runner to prevent
    // leaked interrupts
    int s = state;
    if (s >= INTERRUPTING)
        handlePossibleCancellationInterrupt(s);
}

finally 块确保无论任务是正常完成、异常终止还是被取消,都会执行一些必要的清理和收尾工作。

  • runner = null: 将 runner 字段重置为 null。这非常重要。runner 字段的存在是为了防止并发调用 run(),一旦任务执行完毕(状态不再是 NEW),runner 字段就不再需要指向执行线程了。
  • 处理中断:
    • int s = state;: 重新读取 state为什么要在 runner = null 之后重读 state 这是为了处理一个竞态条件。考虑 cancel(true) 方法,它会尝试中断正在执行任务的线程。cancel(true) 的逻辑是:先设置状态为 INTERRUPTING,然后获取 runner 线程并调用 runner.interrupt()。如果在 run() 方法中,我们在 runner = null 之前读取 state,可能会发生以下情况:
      1. run() 方法:执行完任务,state 还是 NEW(即将变为 NORMAL)。
      2. cancel() 线程:调用 cancel(true),但 run() 线程还未将 runner 设为 null
      3. run() 方法:将 runner 设为 null
      4. cancel() 线程:获取到 runner 为 null,无法中断线程。 这样中断信号就丢失了。 通过在 runner = null 之后重读 state,可以保证如果 cancel(true) 在此期间被调用,state 会被更新为 INTERRUPTING 或 INTERRUPTED
    • if (s >= INTERRUPTING): 检查任务是否在执行期间或执行后被中断。
    • handlePossibleCancellationInterrupt(s): 如果检测到中断状态,这个方法会处理相关的中断逻辑,主要是确保中断状态被正确处理。例如,它会自旋等待,直到状态从 INTERRUPTING 变为 INTERRUPTED,确保 cancel() 的中断操作完成。

任务都执行完了,为什么还要处理中断

答案是:handlePossibleCancellationInterrupt 的核心目的,不是为了让已经执行完的 业务代码 响应中断,而是为了确保 FutureTask 自身作为一个并发组件的 状态机 在任何并发时序下都能正确、一致地运转。

我们来拆解一下这个过程:

1. 中断是什么?cancel(true) 做了什么?

  • 中断是一个协作信号:调用 thread.interrupt() 并不会强行终止线程。它只是在那个线程上设置一个“中断状态”的标志位。正在运行的代码需要自己去检查这个标志位 (Thread.isInterrupted()),并决定如何响应(比如:抛出 InterruptedException、提前退出循环等)。
  • Future.cancel(true) 的意图:它的意图是“请停止你正在做的事情”。它会尝试去中断正在执行这个 FutureTask 的线程。

2. 关键的竞态条件:run() vs cancel(true)

FutureTask 的 run() 方法和 cancel(true) 方法可能会在不同线程中被同时调用,这就产生了一场“竞赛”。最微妙的时刻发生在 run() 方法即将结束的 finally 块中。

我们来看一下 run() 的 finally 块和 cancel(true) 的核心逻辑:

run() 的 finally 块:

finally {
    runner = null; // 1. 清除正在执行的线程记录
    int s = state; // 2. 重新读取状态
    if (s >= INTERRUPTING) // 3. 检查是否被中断
        handlePossibleCancellationInterrupt(s);
}

cancel(true) 的大致逻辑:

// 伪代码
boolean cancel(boolean mayInterruptIfRunning) {
    // ...
    if (mayInterruptIfRunning) {
        // A. 将状态从 NEW 设置为 INTERRUPTING
        // B. 获取 runner 线程
        Thread t = runner; 
        if (t != null)
            t.interrupt(); // C. 中断该线程
        // D. 将状态设置为 INTERRUPTED
    }
    // ...
}

现在,想象一下这个极限时序:

  1. run() 线程:任务的业务逻辑 (callable.call()) 刚刚执行完毕,进入了 finally 块。
  2. cancel() 线程:在此时调用 cancel(true)。它成功地将 state 从 NEW 修改为 INTERRUPTING (步骤 A)。
  3. run() 线程:执行了 runner = null; (步骤 1)。
  4. cancel() 线程:现在尝试获取 runner 线程 (步骤 B),但它读到的是 null!因此,它无法调用 t.interrupt() (步骤 C)。中断信号看似丢失了!
  5. run() 线程:继续执行,它重新读取 state (步骤 2),发现 state 已经是 INTERRUPTING
  6. run() 线程:进入 if 判断,调用 handlePossibleCancellationInterrupt(INTERRUPTING)

即使 cancel() 线程没能成功发出中断,run() 线程也通过 state 的变化感知到了中断的意图。这就是为什么要在 runner = null 之后重新读取 state 的原因,它是为了捕获这种“丢失的中断”。

3. handlePossibleCancellationInterrupt 的作用:等待状态稳定

现在我们来看这个方法的内部:

// ... existing code ...
private void handlePossibleCancellationInterrupt(int s) {
    // It is possible for our interrupter to stall before getting a
    // chance to interrupt us.  Let's spin-wait patiently.
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt
// ... existing code ...
}
  • if (s == INTERRUPTING)run() 线程发现自己捕捉到了一个正在进行中的 cancel 操作。
  • while (state == INTERRUPTING) Thread.yield();: 这是一个“自旋等待”。run() 线程在这里会让出 CPU,耐心地等待。等谁呢?等待那个调用 cancel(true) 的线程完成它剩下的工作,也就是将 state 从 INTERRUPTING 设置为 INTERRUPTED (步骤 D)。

这个等待确保了 FutureTask 的状态机从一个临时状态 (INTERRUPTING) 完整地过渡到了最终状态 (INTERRUPTED)。它同步了 run() 线程和 cancel() 线程,确保当 run() 方法完全退出时,FutureTask 的状态是最终确定的、一致的。

所以,“响应中断”有两层含义:

  1. 对 FutureTask 状态机的响应run() 方法必须确保 FutureTask 的状态能够正确反映出 cancel(true) 的调用。即使业务结果已经产生,但如果 cancel 操作与 run 的完成操作同时发生,FutureTask 必须解决这个状态冲突,并确保最终状态是一致的。handlePossibleCancellationInterrupt 就是这个“状态协调”过程的一部分。
  2. 对线程中断标志的(不)处理:值得注意的是,handlePossibleCancellationInterrupt 方法里有一行被注释掉的代码 // Thread.interrupted();。这意味着 FutureTask 故意不清除 线程的中断标志。它认为自己没有权力这么做,因为这个线程可能是一个线程池的工作线程,线程池本身需要根据中断状态来决定是否要回收这个线程。FutureTask 的职责是完成自身的状态同步,然后把线程的中断状态“原封不动”地留给线程的管理者去处理。

因此,这里的“响应中断”本质上是一个并发组件在面对外部状态变更请求时,进行内部状态同步和确保最终一致性的机制,而不是去影响已经计算出的业务结果。

总结

FutureTask.run() 方法是一个高度并发、状态驱动的实现,其设计精髓在于:

  1. 原子性的所有权确立: 通过对 state 和 runner 的原子操作,确保了任务的“执行一次”语义,即使在多线程并发调用 run() 的情况下也能正确工作。
  2. 健壮的状态管理: 整个执行过程都围绕着内部状态机进行,通过严谨的状态检查和转换,处理了正常执行、异常、取消等多种场景。
  3. 优雅的并发协作: 通过 runner 字段和 state 字段,run() 方法与 cancel() 方法实现了无锁的协作,正确处理了中断信号的传递,避免了竞态条件。
  4. 结果发布与线程唤醒: 通过 set() 和 setException() 方法,安全地发布计算结果,并唤醒所有等待的线程 (get() 调用者)。

获取结果 (get()get(long, TimeUnit) 方法)

  • get() 方法:

    • 首先检查当前状态 s = state

    • 如果任务尚未完成 (s <= COMPLETING),则调用 awaitDone(boolean timed, long nanos) 方法阻塞等待。

    • 一旦任务完成(状态变为 NORMAL, EXCEPTIONAL, CANCELLED, 或 INTERRUPTED),awaitDone 返回,然后 get() 方法调用 report(int s) 来返回结果或抛出相应的异常。

    • NORMAL: 返回结果。

    • EXCEPTIONAL: 抛出 ExecutionException (包装了原始异常)。

    • CANCELLEDINTERRUPTED: 抛出 CancellationException

  • get(long, TimeUnit)** 方法**:类似 get(),但带有超时机制。如果在超时时间内任务未完成,则抛出 TimeoutException

取消任务 (cancel方法)

提供了取消一个尚未完成的任务的能力,其实现同样体现了 Java 并发包一贯的精巧与严谨。

首先,我们看一下 cancel 方法的完整源码:

// ... existing code ...
public boolean cancel(boolean mayInterruptIfRunning) {
    if (state != NEW)
        return false;
    if (mayInterruptIfRunning) {
        if (!STATE.compareAndSet(this, NEW, INTERRUPTING))
            return false;
        try {
            Thread t = runner;
            if (t != null)
                t.interrupt();
        } finally {
            STATE.setRelease(this, INTERRUPTED);
        }
    }
    else if (!STATE.compareAndSet(this, NEW, CANCELLED))
        return false;
    finishCompletion();
    return true;
}
// ... existing code ...

这个方法逻辑清晰,主要分为几个步骤:状态检查根据参数选择取消策略执行中断(如果需要)完成状态转换收尾工作

1. 初始状态检查
if (state != NEW)
    return false;
  • 方法入口的守卫:这是 cancel 方法的第一道防线。一个任务只有在处于 NEW(新建)状态时才能被取消。
  • 为什么?
    • 如果任务已经正常完成 (NORMAL)、或因异常结束 (EXCEPTIONAL),那么它已经有了一个确定的最终结果,取消它已经没有意义了。
    • 如果任务已经被取消 (CANCELLEDINTERRUPTINGINTERRUPTED),那么再次取消也是无效操作。
  • 返回值: 如果任务不处于 NEW 状态,方法直接返回 false,表示“取消失败”,因为任务已经进入了不可取消的终态。
2. 取消策略的选择:mayInterruptIfRunning 参数的意义

这是 cancel 方法最核心的分支逻辑,它完全由 mayInterruptIfRunning 这个布尔参数决定。

分支一:cancel(true) (即 mayInterruptIfRunning 为 true)

这个分支的意图是:如果任务已经在运行,那么就中断那个正在执行它的线程。

if (mayInterruptIfRunning) {
    if (!STATE.compareAndSet(this, NEW, INTERRUPTING))
        return false;
    try {
        Thread t = runner;
        if (t != null)
            t.interrupt();
    } finally {
        STATE.setRelease(this, INTERRUPTED);
    }
}
  1. if (!STATE.compareAndSet(this, NEW, INTERRUPTING)):

    • 首先,它尝试以原子方式将 state 从 NEW 修改为 INTERRUPTINGINTERRUPTING 是一个短暂的中间状态,它表示“取消请求已收到,正在尝试中断执行线程”。
    • 如果这个 CAS 操作失败,说明就在刚才,有另一个线程(可能是 run() 线程或其他 cancel() 调用)已经修改了 state。此时,当前 cancel 调用失败,返回 false
  2. try { ... } finally { ... }:

    • Thread t = runner;: 获取当前正在执行此任务的线程。runner 字段是在 run() 方法开始时被原子设置的。
    • if (t != null) t.interrupt();: 如果 runner 不为 null,说明任务确实已经在某个线程上运行了,此时调用该线程的 interrupt() 方法。这会设置该线程的中断标志位。正在执行的任务代码(callable.call())可以检查这个中断状态,并决定是否要提前退出。
    • finally { STATE.setRelease(this, INTERRUPTED); }: 无论 runner 是否为 null,或者 interrupt() 是否抛出异常(虽然它本身不抛),finally 块都会被执行。它负责将状态从临时的 INTERRUPTING 设置为最终的 INTERRUPTEDsetRelease 是一个带有内存屏障的写操作,确保状态的改变对所有其他线程立即可见。这个 finally 保证了状态机一定会从 INTERRUPTING 前进到 INTERRUPTED,不会被卡在中间状态。
分支二:cancel(false) (即 mayInterruptIfRunning 为 false)

这个分支的意图是:如果任务还没开始,就取消它;如果任务已经开始运行了,那就别管它,让它继续运行下去。

else if (!STATE.compareAndSet(this, NEW, CANCELLED))
    return false;
  • !STATE.compareAndSet(this, NEW, CANCELLED):
    • 它尝试以原子方式将 state 从 NEW 直接修改为 CANCELLED
    • 如果 CAS 成功,意味着在这一刻,run() 方法还没有开始执行(即 runner 线程还未设置),任务被成功“拦截”并设置为取消状态。后续 run() 方法开始执行时,它的入口检查 state != NEW 会失败,任务的业务逻辑也就不会被执行了。
    • 如果 CAS 失败,说明 state 已经不是 NEW 了(很可能是 run() 方法已经开始执行,将 state 变为了 COMPLETING 或者 runner 已经被设置),此时 cancel(false) 不会做任何事,直接返回 false,表示“无法取消,因为它已经在运行了”。
3. 最后的收尾工作
finishCompletion();
return true;
  • finishCompletion(): 只要上述两个分支中的任意一个 CAS 操作成功(即任务状态成功变为了 INTERRUPTED 或 CANCELLED),就说明本次 cancel 调用是成功的。此时,就需要调用 finishCompletion()。我们之前分析过,这个方法会负责唤醒所有正在 get() 方法上等待结果的线程。这些被唤醒的线程会立即从 get() 方法中收到一个 CancellationException
  • return true;: 返回 true,表示取消操作成功。

总结

FutureTask.cancel() 方法是一个设计精良的并发操作,它完美地处理了取消任务时的各种复杂情况:

  1. 原子性和幂等性: 通过 CAS 操作确保了状态转换的原子性。一旦任务进入终态(完成或取消),后续的 cancel 调用都会失败并返回 false,保证了操作的幂等性。
  2. 策略分离mayInterruptIfRunning 参数清晰地分离了两种取消意图:“温和取消”(不打断)和“激进取消”(尝试中断),将选择权交给了调用者。
  3. 状态机严谨性: 使用了 INTERRUPTING 这个中间状态,优雅地处理了 cancel(true) 时的中断过程,并用 finally 块保证了状态机总能达到最终状态 INTERRUPTED
  4. 与 run() 和 get() 的协同cancel 的状态变更能够被 run() 方法及时感知,从而阻止已取消任务的执行。同时,通过 finishCompletion(),它能主动通知所有等待者任务已被取消,构成了完整的并发协作闭环。

等待队列 (WaitNodewaiters 字段)

  • private volatile WaitNode waiters;:这是一个指向等待线程链表头部的指针。这个链表是一个简单的 Treiber 栈 (LIFO 栈)。

  • WaitNode 是一个静态内部类,代码如下:

// ... 
static final class WaitNode { 
    volatile Thread thread; 
    volatile WaitNode next; 
    WaitNode() { thread = Thread.currentThread(); } 
} 
// ... 

每个 WaitNode 封装了一个等待结果的线程 (thread = Thread.currentThread()) 和一个指向下一个节点的指针 (next)。

阻塞机制 (awaitDone() )

awaitDone(boolean timed, long nanos)

当一个线程调用 get() 并且任务未完成时,会进入此方法。它会创建一个新的 WaitNode,然后在一个循环中:

  1. 通过 CAS 将新的 WaitNode 添加到 waiters 链表的头部(实现入栈)。

  2. 再次检查任务状态,如果已完成,则移除刚添加的节点并返回状态。

  3. 如果任务仍未完成,则调用 LockSupport.park(this) (或 LockSupport.parkNanos(this, nanos)) 使当前线程阻塞。

  4. 当线程被唤醒时,如果是因为中断,则从等待队列中移除节点并抛出 InterruptedException。如果是因为超时,则从等待队列中移除节点并返回当前状态。

finishCompletion() 源码分析

我们先来看一下完整的源码:

// ... existing code ...
private void finishCompletion() {
    // assert state > COMPLETING;
    for (WaitNode q; (q = waiters) != null;) {
        if (WAITERS.weakCompareAndSet(this, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }

    done();

    callable = null;        // to reduce footprint
}
// ... existing code ...

下面我们分步解析这个方法的逻辑。

1. 唤醒所有等待的线程

这部分是 finishCompletion 最核心、最复杂的功能。当一个或多个线程调用 FutureTask.get() 方法时,如果任务还没有完成,这些线程就会被阻塞,并被添加到一个等待者队列中。这个队列是一个后进先出(LIFO)的栈式结构,由 waiters 字段指向栈顶。

for (WaitNode q; (q = waiters) != null;) {
    if (WAITERS.weakCompareAndSet(this, q, null)) {
        // ... 唤醒逻辑 ...
        break;
    }
}
  • for (WaitNode q; (q = waiters) != null;): 这是一个外部循环。它首先检查 waiters 是否为 null。如果不为 null,说明有线程正在等待。它将当前的栈顶节点(waiters)赋值给局部变量 q。这个循环会一直尝试,直到成功将等待者队列“抢”到手。
  • if (WAITERS.weakCompareAndSet(this, q, null)): 这是关键的原子操作。WAITERS 是一个 VarHandle(在旧版本 JDK 中是 AtomicReferenceFieldUpdater),它提供了对 waiters 字段的原子CAS(比较并交换)能力。
    • weakCompareAndSet: 它尝试将 waiters 字段从 q(我们刚刚读取到的旧栈顶)原子地更新为 null
    • 为什么这么做? 这是一次性地、原子地将整个等待者链表从 FutureTask 实例上“摘下来”。一旦这个 CAS 操作成功,当前线程就独占了整个等待者队列,FutureTask 的 waiters 字段变回了 null。这样做的好处是,即使在唤醒过程中有新的线程调用 get(),它们会看到 waiters 是 null(或者是一个新的、与当前处理的队列无关的节点),从而避免了复杂的并发处理。
    • 为什么是 weak weakCompareAndSet 允许“伪失败”(spurious failure),即即使值没有被其他线程改变,也可能返回 false。在这种并发场景下,外部的 for 循环会简单地重试,所以使用 weak 版本是安全且可能更高效的。
  • break;: 一旦成功获取并清空了等待者队列,就跳出外层循环,因为唤醒工作只需要做一次。

接下来是内部的唤醒循环:

for (;;) {
    Thread t = q.thread;
    if (t != null) {
        q.thread = null;
        LockSupport.unpark(t);
    }
    WaitNode next = q.next;
    if (next == null)
        break;
    q.next = null; // unlink to help gc
    q = next;
}
  • for (;;): 这是一个无限循环,用于遍历刚刚“摘下来”的等待者链表(头节点是 q)。
  • Thread t = q.thread;: 获取当前 WaitNode 节点中包装的等待线程。
  • if (t != null): 检查线程是否为 null
  • q.thread = null;: 在唤醒之前将节点中的线程引用置为 null
  • LockSupport.unpark(t);: 这是实际的唤醒操作。unpark 会让因为调用 LockSupport.park() 而阻塞的线程 t 恢复运行。调用 get() 的线程就是在内部调用了 park 来等待的。
  • 遍历链表:
    • WaitNode next = q.next;: 获取链表中的下一个节点。
    • if (next == null) break;: 如果没有下一个节点了,说明所有等待者都已处理完毕,跳出内层循环。
    • q.next = null; // unlink to help gc这是一个帮助 GC 的优化。将当前处理完的节点 q 的 next 指针断开,使其成为一个孤立的对象,这样垃圾回收器就可以更容易地回收它,避免因为链表引用而导致整个链表无法被回收。
    • q = next;: 移动到下一个节点继续处理。

这个过程确保了所有在任务完成前调用 get() 并陷入等待的线程,都会被安全、高效地唤醒,并能够获取到任务的结果。

2. 执行 done() 回调钩子
done();
  • done() 是一个 protected 方法,它在 FutureTask 内部是一个空实现。
  • 这是一个模板方法模式的应用。它允许开发者通过继承 FutureTask 并重写 done() 方法,来定义任务完成时需要执行的自定义逻辑。例如,你可以在这里更新 UI、记录日志、触发下一个任务等。
  • 因为 done() 是在任务结果已经确定、所有等待者都已被安排唤醒之后调用的,所以它是一个非常可靠的回调点。
3. 清理资源
callable = null;        // to reduce footprint
  • 任务执行完毕后,callable 对象(包含了具体的业务逻辑)就不再需要了。
  • 将其设置为 null 是一个重要的内存优化。如果不这么做,FutureTask 对象会一直持有对 callable 的引用。如果 callable 是一个复杂的对象,或者是一个匿名内部类隐式持有了外部大对象的引用,那么这部分内存将无法被垃圾回收,可能导致内存泄漏。
  • 通过在这里将其置 null,可以减小 FutureTask 对象的内存占用(footprint),并帮助 GC 更快地回收不再需要的资源。

总结

finishCompletion() 方法是 FutureTask 设计中的点睛之笔,它优雅地处理了任务完成后的所有收尾工作:

  1. 并发安全: 使用原子操作(CAS)来获取等待者队列,实现了无锁的线程唤醒,既高效又安全。
  2. 责任分离: 将“计算结果”和“通知等待者”这两个职责清晰地分离开来。
  3. 可扩展性: 提供了 done() 这个 protected 的钩子方法,使得 FutureTask 的行为可以被方便地扩展。
  4. 资源管理: 主动清理不再需要的 callable 引用,体现了良好的内存管理意识。

哪个线程负责管理唤醒 get 等待的线程?

负责唤醒等待线程的是完成任务的那个线程。具体来说:

  • 如果是任务正常执行完成或抛出异常,那么是执行 run() 方法的线程(即 runner 线程)在调用 set()setException() 后,最终会调用 finishCompletion() 来唤醒所有等待者。

  • 如果是任务被取消,那么是调用 cancel() 方法的线程在成功取消任务后,会调用 finishCompletion() 来唤醒所有等待者。

等待 get() 方法结果的线程被封装在 WaitNode 对象中。每个 WaitNode 包含:

  • volatile Thread thread;: 对等待线程本身的引用。

  • volatile WaitNode next;: 指向链表中下一个 WaitNode 的引用。

这些 WaitNode 对象形成一个后进先出 (LIFO) 的栈式链表,其头节点由 FutureTaskvolatile WaitNode waiters; 字段指向。当一个线程需要等待时,它会创建一个新的 WaitNode 并将其 CAS 到 waiters 链表的头部。当任务完成时,finishCompletion() 方法会遍历这个链表并唤醒每个节点中的线程。

设计优势

这种设计避免了使用更重的锁(如 AbstractQueuedSynchronizer,早期版本的 FutureTask 使用过它),转而使用轻量级的 CAS 操作和 LockSupport 进行线程的阻塞和唤醒,这在很多情况下能提供更好的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值