我们来深入解析 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()方法来获取结果的线程。
现在,看这个“完美风暴”般的时序:
- T2 (
get): 调用get(),检查state,发现是NEW。它判断任务还没完成,于是准备将自己加入到waiters等待队列中,然后挂起(park)。 - T1 (
set): 在 T2 准备挂起但还没来得及挂起的时候,T1 抢到了 CPU 时间片。 - T1 (
set): T1 飞快地执行完了整个set方法:- 设置
outcome = result。 - CAS 操作成功,将
state从NEW改为NORMAL。 - 调用
finishCompletion()。finishCompletion检查waiters队列,发现是null(因为 T2 还没来得及把自己加进去),所以它什么也不做就返回了。
- 设置
- 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);
}
}
现在,我们用正确的逻辑重演刚才的“完美风暴”:
- T2 (
get): 调用get(),检查state,发现是NEW。 - T1 (
set): 抢到 CPU,执行STATE.compareAndSet(this, NEW, COMPLETING)。这个操作成功了!现在state是COMPLETING。 - T2 (
get): 再次获得 CPU,它在循环里重新检查state,发现state变成了COMPLETING! - T2 (
get): 命中了else if (s == COMPLETING)这个分支。它不会去挂起自己,而是调用Thread.yield(),表示“我知道结果马上就好了,我稍微等一下,让出 CPU,下一轮再来看看”。 - T1 (
set): 继续执行,设置outcome,然后将state设置为NORMAL,最后调用finishCompletion()。 - T2 (
get): 在下一次循环中,T2 检查state,发现已经是NORMAL(s > COMPLETING),于是它直接读取outcome并正常返回。
危机解除。COMPLETING 状态在这里起到了一个至关重要的屏障作用。它告诉其他正在调用 get() 的线程:“别睡!结果马上就好,稍微自旋(spin-wait)一下!”。通过这种方式,它完美地避免了在设置结果和唤醒等待者之间的微小时间窗口内发生“丢失的唤醒”问题。
所以,COMPLETING 是一个短暂但必需的中间状态,它的存在是为了确保 FutureTask 在高并发下的绝对健壮性。
任务执行 (run() 方法)
当 FutureTask 的 run() 方法被调用时(通常由一个 Executor 的工作线程调用):
-
首先会通过 CAS 操作尝试将
runner字段(volatile Thread runner)从null设置为当前线程。这确保了只有一个线程可以实际执行任务。 -
如果设置成功并且任务状态是
NEW,则会调用内部的Callable对象的call()方法。 -
如果
call()方法正常返回,则调用set(V result)方法设置结果,并将状态转换为NORMAL。 -
如果
call()方法抛出异常,则调用setException(Throwable t)方法设置异常,并将状态转换为EXCEPTIONAL。 -
在
finally块中,runner字段会被重置为null。还会检查任务是否在运行期间被取消并需要中断(状态为INTERRUPTING或INTERRUPTED),如果是,则会调用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字段表示当前任务的状态(NEW,COMPLETING,NORMAL,EXCEPTIONAL,CANCELLED,INTERRUPTING,INTERRUPTED)。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,避免了任务被重复执行。
- 为什么需要 CAS? 这解决了并发执行
这个入口守卫是确保 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);
}
}
// ...
这部分是实际执行任务的逻辑:
- 获取
Callable:FutureTask在构造时会接收一个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,可能会发生以下情况:run()方法:执行完任务,state还是NEW(即将变为NORMAL)。cancel()线程:调用cancel(true),但run()线程还未将runner设为null。run()方法:将runner设为null。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
}
// ...
}
现在,想象一下这个极限时序:
run()线程:任务的业务逻辑 (callable.call()) 刚刚执行完毕,进入了finally块。cancel()线程:在此时调用cancel(true)。它成功地将state从NEW修改为INTERRUPTING(步骤 A)。run()线程:执行了runner = null;(步骤 1)。cancel()线程:现在尝试获取runner线程 (步骤 B),但它读到的是null!因此,它无法调用t.interrupt()(步骤 C)。中断信号看似丢失了!run()线程:继续执行,它重新读取state(步骤 2),发现state已经是INTERRUPTING。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 的状态是最终确定的、一致的。
所以,“响应中断”有两层含义:
- 对
FutureTask状态机的响应:run()方法必须确保FutureTask的状态能够正确反映出cancel(true)的调用。即使业务结果已经产生,但如果cancel操作与run的完成操作同时发生,FutureTask必须解决这个状态冲突,并确保最终状态是一致的。handlePossibleCancellationInterrupt就是这个“状态协调”过程的一部分。 - 对线程中断标志的(不)处理:值得注意的是,
handlePossibleCancellationInterrupt方法里有一行被注释掉的代码// Thread.interrupted();。这意味着FutureTask故意不清除 线程的中断标志。它认为自己没有权力这么做,因为这个线程可能是一个线程池的工作线程,线程池本身需要根据中断状态来决定是否要回收这个线程。FutureTask的职责是完成自身的状态同步,然后把线程的中断状态“原封不动”地留给线程的管理者去处理。
因此,这里的“响应中断”本质上是一个并发组件在面对外部状态变更请求时,进行内部状态同步和确保最终一致性的机制,而不是去影响已经计算出的业务结果。
总结
FutureTask.run() 方法是一个高度并发、状态驱动的实现,其设计精髓在于:
- 原子性的所有权确立: 通过对
state和runner的原子操作,确保了任务的“执行一次”语义,即使在多线程并发调用run()的情况下也能正确工作。 - 健壮的状态管理: 整个执行过程都围绕着内部状态机进行,通过严谨的状态检查和转换,处理了正常执行、异常、取消等多种场景。
- 优雅的并发协作: 通过
runner字段和state字段,run()方法与cancel()方法实现了无锁的协作,正确处理了中断信号的传递,避免了竞态条件。 - 结果发布与线程唤醒: 通过
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(包装了原始异常)。 -
CANCELLED或INTERRUPTED: 抛出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),那么它已经有了一个确定的最终结果,取消它已经没有意义了。 - 如果任务已经被取消 (
CANCELLED,INTERRUPTING,INTERRUPTED),那么再次取消也是无效操作。
- 如果任务已经正常完成 (
- 返回值: 如果任务不处于
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);
}
}
-
if (!STATE.compareAndSet(this, NEW, INTERRUPTING)):- 首先,它尝试以原子方式将
state从NEW修改为INTERRUPTING。INTERRUPTING是一个短暂的中间状态,它表示“取消请求已收到,正在尝试中断执行线程”。 - 如果这个 CAS 操作失败,说明就在刚才,有另一个线程(可能是
run()线程或其他cancel()调用)已经修改了state。此时,当前cancel调用失败,返回false。
- 首先,它尝试以原子方式将
-
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设置为最终的INTERRUPTED。setRelease是一个带有内存屏障的写操作,确保状态的改变对所有其他线程立即可见。这个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() 方法是一个设计精良的并发操作,它完美地处理了取消任务时的各种复杂情况:
- 原子性和幂等性: 通过 CAS 操作确保了状态转换的原子性。一旦任务进入终态(完成或取消),后续的
cancel调用都会失败并返回false,保证了操作的幂等性。 - 策略分离:
mayInterruptIfRunning参数清晰地分离了两种取消意图:“温和取消”(不打断)和“激进取消”(尝试中断),将选择权交给了调用者。 - 状态机严谨性: 使用了
INTERRUPTING这个中间状态,优雅地处理了cancel(true)时的中断过程,并用finally块保证了状态机总能达到最终状态INTERRUPTED。 - 与
run()和get()的协同:cancel的状态变更能够被run()方法及时感知,从而阻止已取消任务的执行。同时,通过finishCompletion(),它能主动通知所有等待者任务已被取消,构成了完整的并发协作闭环。
等待队列 (WaitNode 和 waiters 字段)
-
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,然后在一个循环中:
-
通过 CAS 将新的
WaitNode添加到waiters链表的头部(实现入栈)。 -
再次检查任务状态,如果已完成,则移除刚添加的节点并返回状态。
-
如果任务仍未完成,则调用
LockSupport.park(this)(或LockSupport.parkNanos(this, nanos)) 使当前线程阻塞。 -
当线程被唤醒时,如果是因为中断,则从等待队列中移除节点并抛出
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 设计中的点睛之笔,它优雅地处理了任务完成后的所有收尾工作:
- 并发安全: 使用原子操作(CAS)来获取等待者队列,实现了无锁的线程唤醒,既高效又安全。
- 责任分离: 将“计算结果”和“通知等待者”这两个职责清晰地分离开来。
- 可扩展性: 提供了
done()这个protected的钩子方法,使得FutureTask的行为可以被方便地扩展。 - 资源管理: 主动清理不再需要的
callable引用,体现了良好的内存管理意识。
哪个线程负责管理唤醒 get 等待的线程?
负责唤醒等待线程的是完成任务的那个线程。具体来说:
-
如果是任务正常执行完成或抛出异常,那么是执行
run()方法的线程(即runner线程)在调用set()或setException()后,最终会调用finishCompletion()来唤醒所有等待者。 -
如果是任务被取消,那么是调用
cancel()方法的线程在成功取消任务后,会调用finishCompletion()来唤醒所有等待者。
等待 get() 方法结果的线程被封装在 WaitNode 对象中。每个 WaitNode 包含:
-
volatile Thread thread;: 对等待线程本身的引用。 -
volatile WaitNode next;: 指向链表中下一个WaitNode的引用。
这些 WaitNode 对象形成一个后进先出 (LIFO) 的栈式链表,其头节点由 FutureTask 的 volatile WaitNode waiters; 字段指向。当一个线程需要等待时,它会创建一个新的 WaitNode 并将其 CAS 到 waiters 链表的头部。当任务完成时,finishCompletion() 方法会遍历这个链表并唤醒每个节点中的线程。
设计优势
这种设计避免了使用更重的锁(如 AbstractQueuedSynchronizer,早期版本的 FutureTask 使用过它),转而使用轻量级的 CAS 操作和 LockSupport 进行线程的阻塞和唤醒,这在很多情况下能提供更好的性能。


1215

被折叠的 条评论
为什么被折叠?



