为什么需要 COMPLETING
1. 保证结果设置和状态更新的原子性
FutureTask 的设计初衷是确保线程安全性和结果一致性。COMPLETING 是一个短暂的中间状态,用来确保:
- 任务完成后,首先安全地写入结果(或异常)。
- 然后再将状态更新为
NORMAL或EXCEPTIONAL。
通过引入 COMPLETING 状态,可以分阶段地完成任务的收尾工作,避免直接从 NEW 到 NORMAL 的状态跳转可能引发的问题。
2. 解决并发访问问题
如果没有 COMPLETING,在高并发场景下可能会发生以下问题:
- 一个线程正在完成任务(写入结果),但状态还未更新到
NORMAL。 - 另一个线程检测到任务完成(因为它看到状态改变),却可能读取到不完整的结果。
COMPLETING 的作用就是防止其他线程认为任务已经完成,但结果还未写入。
3. 线程安全的等待机制
FutureTask 提供了阻塞方法 get(),等待任务完成并获取结果。如果没有 COMPLETING 状态,get() 调用可能会在任务结果还未完全写入时返回,导致读取到未定义的结果。
FutureTask 中的状态更新过程
以下是任务完成时的关键代码:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v; // 设置任务结果
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 更新状态为 NORMAL
finishCompletion(); // 唤醒等待线程
}
}
可以看到:
- 状态从
NEW改为COMPLETING时,任务结果尚未设置。 - 确保结果(
outcome)写入后,状态再更新为NORMAL。
这一步分离了“结果写入”和“完成标记”,利用 COMPLETING 作为中间状态避免了潜在的竞争问题。
核心问题:状态更新和结果设置的“竞态条件”
在并发环境中,如果没有 COMPLETING 状态,任务完成的流程可能会出问题。以下是关键问题点:
假设没有 COMPLETING 状态,直接从 NEW 跳到 NORMAL 或 EXCEPTIONAL
- 任务结果还未完全设置,但状态已被标记为完成 (
NORMAL或EXCEPTIONAL)。 - 其他线程可能看到状态已完成,就会立即去读取结果,而此时结果可能尚未写入(或不完整)。
- 导致读取到未定义的结果,甚至程序崩溃。
关键点:
- 状态改变(任务完成)和 结果写入(
outcome的设置)是两个操作。 - 这两个操作需要严格按顺序完成,但如果没有一个“中间状态”进行过渡,就容易发生线程间的竞态条件。
有 COMPLETING 的解决方案
通过引入 COMPLETING,可以将状态更新拆分为两个阶段:
- 状态从
NEW转为COMPLETING,告诉其他线程“任务快完成了,但结果还没完全准备好”。 - 确保结果写入成功后,再将状态改为最终的
NORMAL或EXCEPTIONAL。
这样,其他线程在看到 COMPLETING 时,就知道需要等待结果完全写入,而不会直接读取。
COMPLETING 状态的精髓:分离“结果写入”和“状态完成”的责任
1. 状态转换的两阶段性
COMPLETING 是一种 信号,用来标记:
任务已经执行完毕,结果正在写入中。请稍等,马上就好!
它保证了:
- 第一阶段:结果写入是独立且线程安全的,不会受到其他线程干扰。
- 第二阶段:任务标记为完成后,其他线程可以放心读取结果。
没有
COMPLETING,状态转换会直接跳过第一阶段,导致线程读取未写入完全的结果。
2. 并发场景下的一致性
在多线程环境中,状态和结果的更新必须遵循以下约束:
- 状态从
NEW到最终状态必须是线性且可预测的。 - 其他线程只能在任务完成后读取结果,不能提前。
COMPLETING 的引入确保了这两个约束不会被打破。
举个类比:快递派送过程
想象你在等待一个快递,任务的状态转换可以用以下类比表示:
NEW:快递正在路上(任务未开始执行)。COMPLETING:快递员正在派送快递,但你还没签收(结果正在写入中)。NORMAL:你签收了快递(任务完成,结果可用)。EXCEPTIONAL:快递丢了(任务失败)。
如果没有“派送中”(COMPLETING)这个状态,快递公司可能直接告诉你快递已签收,但实际上你还没收到快递。这显然是不合理的。
实际代码中的流程拆解
来看 FutureTask 中任务完成的核心逻辑:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v; // 设置结果
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 标记任务完成
finishCompletion(); // 唤醒等待线程
}
}
分解成两步:
-
compareAndSwapInt(NEW, COMPLETING)- 先将状态改为
COMPLETING,表示任务结果正在写入中,其他线程知道暂时不能读取结果。
- 先将状态改为
-
outcome = v;和putOrderedInt(this, stateOffset, NORMAL)- 先写入结果,然后将状态改为
NORMAL,确保任务完成时结果已经完全可用。
- 先写入结果,然后将状态改为
如果直接跳过 COMPLETING 会怎样?
假设直接从 NEW 到 NORMAL:
- 当状态变为
NORMAL时,结果(outcome)可能还未写入。 - 其他线程看到状态已完成,可能直接读取到未定义的结果。
- 导致数据不一致,出现难以调试的并发问题。
我们来详细看看,如果没有 COMPLETING 状态,直接采用“先设置 outcome 再改状态”的方案,可能会发生什么问题:
假设场景
- 线程 A 执行任务,设置了
outcome的值。 - 线程 A 还没来得及更新状态到
NORMAL。 - 线程 B 调用了
get()方法,看到状态还未改变,可能会阻塞等待,或者判断出错。
问题点
-
线程 B 对任务状态和结果的观察会产生模糊性,即:
结果已经写入,但状态未更新,其他线程无法感知结果已可用。 -
在高并发环境中,任务的完成状态需要保证是原子的,也就是:
- 当状态标记为完成时,结果必须是可用且一致的。
- 其他线程看到状态改变,应该能够确定结果已完全设置。
如果没有 COMPLETING 状态,这种“结果写入和状态更新”之间的非原子行为会引发并发错误。
为什么引入 COMPLETING 可以解决问题?
分离阶段
引入 COMPLETING 状态后,任务完成被分为两个阶段:
-
阶段 1:从
NEW转为COMPLETING- 任务进入一个“结果正在写入中”的状态,其他线程可以感知到任务即将完成,但结果尚未完全可用。
-
阶段 2:从
COMPLETING转为NORMAL或EXCEPTIONAL- 结果已完全写入,状态标记为完成。
这种分阶段的处理确保了任务的状态转换是清晰且可预测的。
防止状态提前暴露
- 如果没有
COMPLETING状态,结果设置可能在状态更新之前完成,其他线程会因为未感知到状态更新而误判任务未完成。 COMPLETING的作用是一个过渡信号,告诉其他线程“任务结果正在生成,请稍等”,避免了这种误判。
实际实现中的一致性保障
来看 FutureTask 的完整实现:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v; // 设置结果
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 更新状态为 NORMAL
finishCompletion(); // 唤醒等待的线程
}
}
-
从
NEW转为COMPLETING- 在状态更新之前,先锁定当前任务状态,告诉其他线程结果正在写入中。
- 避免其他线程误以为任务还未开始完成。
-
写入结果
- 将
outcome设置为任务的最终结果。
- 将
-
从
COMPLETING转为NORMAL- 在确保结果已完全写入后,状态才标记为完成。
- 保证结果和状态对所有线程来说是同步可见的。
为什么不能直接跳过 COMPLETING?
核心问题是:直接跳过 COMPLETING,会让状态和结果之间的关系变得模糊。具体来说:
-
状态更新的原子性问题
- 状态从
NEW直接到NORMAL,结果在中途设置,这两个操作并不是一个原子操作。 - 如果没有一个明确的过渡状态,其他线程在结果设置未完成时可能会错误地读取任务状态。
- 状态从
-
并发安全问题
- 在多线程环境下,其他线程可能会同时观察任务的状态和结果。
如果没有COMPLETING状态,任务完成的边界变得模糊,会增加出错的可能性。
- 在多线程环境下,其他线程可能会同时观察任务的状态和结果。
-
清晰的状态流转
- 引入
COMPLETING状态,明确了任务从开始到完成的每一个阶段,便于其他线程判断任务是否已完成。
- 引入
为什么不需要 volatile 修饰 outcome?
1. COMPLETING 状态与内存屏障
在 FutureTask 中,状态的更新(如从 NEW 到 COMPLETING,再到 NORMAL)是通过 compareAndSwapInt(CAS 操作)实现的。CAS 操作隐含了一种 内存屏障(Memory Barrier) ,其效果是:
- 保证在状态更新前,写入的所有变量对其他线程可见。
- 保证在状态更新后,其他线程看到的状态是最新的。
因此,outcome 的值在设置完成后,通过状态更新的内存屏障,对其他线程是可见的。
2. 状态检查的 happens-before 关系
在 FutureTask 中,其他线程在读取 outcome 之前,通常会先检查任务的状态。以下两种情况都能确保可见性:
- 当状态为
NORMAL或EXCEPTIONAL时
此时任务已经完成,状态更新保证了outcome的写操作对其他线程可见。 - 调用
get()时
如果任务未完成,get()方法会阻塞直到状态变为完成(NORMAL或EXCEPTIONAL),此时状态更新后的内存屏障确保了outcome的值对读取线程可见。
3. 结果读取前的 happens-before 关系
无论是通过阻塞(get() 方法)还是非阻塞的方式检查状态,FutureTask 都通过 显式同步机制 建立了 outcome 的写入和读取之间的 happens-before 关系:
- 写入
outcome之前,状态会从NEW转为COMPLETING。 - 状态从
COMPLETING转为NORMAL或EXCEPTIONAL后,其他线程可以读取outcome。 - 状态的更新和
outcome的写入在同一个线程中按顺序发生,其他线程通过内存屏障观察到状态变化时,outcome的值一定已经写入。
代码中的关键路径分析
以下是 FutureTask 完成任务时的核心逻辑:
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v; // 设置结果
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 更新状态为 NORMAL
finishCompletion(); // 唤醒等待线程
}
}
流程说明:
-
compareAndSwapInt将状态从NEW转为COMPLETING:- 这一步含有内存屏障,确保之前的写入(包括
outcome的设置)对其他线程可见。
- 这一步含有内存屏障,确保之前的写入(包括
-
outcome = v:- 设置任务的结果,此时结果写入内存。
-
putOrderedInt将状态设置为NORMAL:- 这是一个有序写(Ordered Write),保证
outcome的写入发生在状态更新之前。 - 其他线程通过状态检查(状态变为
NORMAL)时,一定能够看到最新的outcome。
- 这是一个有序写(Ordered Write),保证
为什么不用 volatile?
-
volatile的开销volatile会引入更多的内存屏障,可能对性能产生额外影响。- 在
FutureTask的设计中,状态更新本身已经隐含了内存屏障,没必要额外使用volatile。
-
内存屏障已确保可见性
compareAndSwapInt和putOrderedInt操作已经提供了足够的同步保证。- 这些操作隐式建立了
outcome的写入和状态读取之间的 happens-before 关系。
-
减少复杂性
volatile的引入可能会让代码显得更复杂,而通过状态更新的内存屏障设计,可以更自然地保证outcome的可见性。
其他线程如何读取 outcome?
以下是 get() 方法的实现简化版:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING) // 如果任务未完成
s = awaitDone(false, 0L); // 等待完成
return report(s);
}
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
关键点:
-
等待完成的机制
- 如果状态未完成,
get()会阻塞,直到状态变为NORMAL或EXCEPTIONAL。 - 阻塞完成后,状态的变化会通过内存屏障确保对读取线程可见。
- 如果状态未完成,
-
读取结果
- 状态变为
NORMAL后,结果outcome已经写入,且对当前线程可见。
- 状态变为