Future.get()
的内存可见性保障
Java 文档中关于 Future.get()
的说明指出,异步计算中的操作(如 Callable
任务)在 Future.get()
调用返回后,对调用线程后续的操作是可见的。具体来说,这是通过 Happens-Before 规则保证的:异步计算线程的所有操作(如修改共享变量)在时间上先于(Happens-Before)调用 Future.get()
线程的后续操作。换句话说,当 get()
成功返回结果时,异步计算中的所有写入操作对调用线程之后的读取操作一定是可见的。
关键点:
-
Happens-Before 规则:
Java 内存模型通过 Happens-Before 规则确保多线程操作的可见性。例如:- 锁的释放 Happens-Before 锁的获取。
- 对
volatile
变量的写操作 Happens-Before 后续对它的读操作。 - 线程启动(
Thread.start()
)Happens-Before 线程内的操作。
-
Future.get()
的作用:
当线程 A 调用Future.get()
获取异步计算结果时,如果任务尚未完成,线程 A 会阻塞;当线程 B 完成计算后,线程 A 被唤醒并获取结果。此时,线程 B 在计算过程中对共享变量的修改,对线程 A 是可见的。
一、底层实现
Future.get()
之所以能够建立 happen-before 关系,是因为其底层实现依赖于 Java 并发工具(如 FutureTask
)的同步机制,这些机制通过 锁 和 内存屏障 来保证多线程环境下的内存可见性。
Future<String> future = executor.submit(new Callable<String>() {public String call() {return searcher.search(target);}});// 等价于
FutureTask<String> future =new FutureTask<String>(new Callable<String>() {public String call() {return searcher.search(target);}});
executor.execute(future);
类图
二、Happen-Before 规则的核心作用
在 Java 内存模型(JMM)中,happen-before 规则确保:
- 可见性:如果操作 A happen-before 操作 B,则 A 对内存的修改对 B 可见。
- 顺序性:操作 A 的执行顺序在操作 B 之前。
Future.get()
通过同步机制保证:
任务线程中的所有操作 happen-before 调用 get()
之后的代码。
三、Future.get()
的具体流程
FutureTask.get() 核心如下:
public V get() throws InterruptedException, ExecutionException {int s = state;if (s <= COMPLETING)s = awaitDone(false, 0L);return report(s);
}
该方法的主要流程:
关键步骤:
-
任务线程完成计算
- 修改
state
为完成状态(volatile
写)。 - 写入计算结果到内存。
- 释放锁(或调用
unpark
)唤醒等待线程。
- 修改
-
主线程调用
get()
- 读取
state
(volatile
读),触发内存屏障。 - 如果任务未完成,通过
LockSupport.park()
阻塞。 - 被唤醒后,再次读取
state
和结果。
- 读取
四、具体代码
JDK 如何通过 FutureTask
实现这一机制?
FutureTask
是 Future
接口的默认实现,其核心通过 状态机 和 volatile
变量 来保证内存一致性。
1. 状态变量 state
(volatile
修饰)
FutureTask
内部维护一个 volatile
修饰的状态变量 state
,表示任务状态(如未启动、已完成、已取消等):
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2; // 正常完成
private static final int EXCEPTIONAL = 3; // 异常完成
private static final int CANCELLED = 4; // 已取消
private static final int INTERRUPTING = 5; // 中断中
private static final int INTERRUPTED = 6; // 已中断
内存可见性保证:
- 写操作:任务执行线程(线程 B)在完成计算后,会通过
volatile
写将state
更新为NORMAL
或EXCEPTIONAL
。 - 读操作:调用
get()
的线程(线程 A)会通过volatile
读检查state
是否完成。 - Happens-Before:对
state
的volatile
写操作 Happens-Before 后续对它的读操作。因此,线程 B 在更新state
前的所有操作(如修改共享变量),对线程 A 读取state
后的操作是可见的。
2. 结果存储与发布
FutureTask
将计算结果存储在 outcome
变量中:
private Object outcome; // 非 volatile
安全发布:
虽然 outcome
不是 volatile
,但通过 volatile
变量 state
的写操作保证其可见性:
- 线程 B 执行任务,将结果写入
outcome
。 - 线程 B 将
state
更新为COMPLETING
,再立即更新为NORMAL
(通过volatile
写)。 - 线程 A 调用
get()
时,发现state
为NORMAL
,会读取outcome
的值。
由于 volatile
写(state
的更新)Happens-Before volatile
读(state
的检查),线程 B 对 outcome
的写入对线程 A 是可见的。
3. 等待与唤醒机制
如果任务未完成,调用 get()
的线程会通过 awaitDone()
进入阻塞:
private int awaitDone(boolean timed, long nanos) throws InterruptedException {WaitNode q = null;boolean queued = false;for (;;) {if (Thread.interrupted()) {removeWaiter(q);throw new InterruptedException();}int s = state;if (s > COMPLETING) { // 任务已完成return s;} else if (s == COMPLETING) { // 任务即将完成Thread.yield();} else {// 将当前线程加入等待队列并阻塞LockSupport.park(this);}}
}
唤醒机制:
任务完成后,线程 B 调用 finishCompletion()
,唤醒所有等待线程:
private void finishCompletion() {for (WaitNode q; (q = waiters) != null;) {if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {for (;;) {Thread t = q.thread;if (t != null) {q.thread = null;LockSupport.unpark(t); // 唤醒线程}// 遍历等待队列并唤醒所有线程}break;}}
}
锁与内存屏障:
通过 LockSupport.unpark()
唤醒线程时,隐式插入了内存屏障,确保线程 B 的写操作对线程 A 可见。
五、总结
volatile
状态变量state
:
保证任务状态的可见性,确保异步计算的写操作在Future.get()
调用后可见。- 安全发布结果:
通过volatile
写(更新state
)发布非volatile
的outcome
变量。 - 等待与唤醒:
使用LockSupport
实现线程阻塞与唤醒,结合内存屏障保证可见性。
通过这些机制,FutureTask
实现了 Future.get()
的内存一致性。