Future.get() 的内存可见性保障(二)

news/2025/3/27 13:23:51/文章来源:https://www.cnblogs.com/bestjosephine/p/18790443

Future.get() 的内存可见性保障

Java 文档中关于 Future.get() 的说明指出,异步计算中的操作(如 Callable 任务)在 Future.get() 调用返回后,对调用线程后续的操作是可见的。具体来说,这是通过 Happens-Before 规则保证的:异步计算线程的所有操作(如修改共享变量)在时间上先于(Happens-Before)调用 Future.get() 线程的后续操作。换句话说,当 get() 成功返回结果时,异步计算中的所有写入操作对调用线程之后的读取操作一定是可见的。

关键点:

  1. Happens-Before 规则
    Java 内存模型通过 Happens-Before 规则确保多线程操作的可见性。例如:

    • 锁的释放 Happens-Before 锁的获取
    • volatile 变量的写操作 Happens-Before 后续对它的读操作
    • 线程启动(Thread.start())Happens-Before 线程内的操作
  2. 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);

类图

classDiagramclass Future~V~ {<<interface>>+cancel(boolean mayInterruptIfRunning) boolean+isCancelled() boolean+isDone() boolean+get() V+get(long timeout, TimeUnit unit) V}class Runnable {<<interface>>+run() void}class RunnableFuture~V~ {<<interface>>+run() void}class FutureTask~V~ {-callable Callable~V~-state int-runner Thread-waiters WaitNode-outcome Object+FutureTask(Callable~V~ callable)+FutureTask(Runnable runnable, V result)+isCancelled() boolean+isDone() boolean+cancel(boolean mayInterruptIfRunning) boolean+get() V+get(long timeout, TimeUnit unit) V+run() void#set(V v) void#setException(Throwable t) void-report(int s) V}Future <|-- RunnableFutureRunnableFuture <|.. FutureTaskRunnable <|-- RunnableFuture

二、Happen-Before 规则的核心作用

在 Java 内存模型(JMM)中,happen-before 规则确保:

  1. 可见性:如果操作 A happen-before 操作 B,则 A 对内存的修改对 B 可见。
  2. 顺序性:操作 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);
}

该方法的主要流程:

sequenceDiagramparticipant Main as 主线程participant Future as Future对象participant Worker as 任务线程Main->>Future: get()Future-->>Future: 检查状态是否完成alt 未完成Future->>Future: awaitDone 通过 LockSupport.park() 阻塞Worker->>Future: 修改 state 为完成状态Worker->>Future: report 通过 LockSupport.unpark() 唤醒主线程endFuture-->>Main: 返回结果

关键步骤:

  1. 任务线程完成计算

    • 修改 state 为完成状态(volatile 写)。
    • 写入计算结果到内存。
    • 释放锁(或调用 unpark)唤醒等待线程。
  2. 主线程调用 get()

    • 读取 statevolatile 读),触发内存屏障。
    • 如果任务未完成,通过 LockSupport.park() 阻塞。
    • 被唤醒后,再次读取 state 和结果。

四、具体代码

JDK 如何通过 FutureTask 实现这一机制?

FutureTaskFuture 接口的默认实现,其核心通过 状态机volatile 变量 来保证内存一致性。

1. 状态变量 statevolatile 修饰)

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 更新为 NORMALEXCEPTIONAL
  • 读操作:调用 get() 的线程(线程 A)会通过 volatile 读检查 state 是否完成。
  • Happens-Before:对 statevolatile 写操作 Happens-Before 后续对它的读操作。因此,线程 B 在更新 state 前的所有操作(如修改共享变量),对线程 A 读取 state 后的操作是可见的。

2. 结果存储与发布

FutureTask 将计算结果存储在 outcome 变量中:

private Object outcome; // 非 volatile

安全发布
虽然 outcome 不是 volatile,但通过 volatile 变量 state 的写操作保证其可见性:

  1. 线程 B 执行任务,将结果写入 outcome
  2. 线程 B 将 state 更新为 COMPLETING,再立即更新为 NORMAL(通过 volatile 写)。
  3. 线程 A 调用 get() 时,发现 stateNORMAL,会读取 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 可见。


五、总结

  1. volatile 状态变量 state
    保证任务状态的可见性,确保异步计算的写操作在 Future.get() 调用后可见。
  2. 安全发布结果
    通过 volatile 写(更新 state)发布非 volatileoutcome 变量。
  3. 等待与唤醒
    使用 LockSupport 实现线程阻塞与唤醒,结合内存屏障保证可见性。

通过这些机制,FutureTask 实现了 Future.get() 的内存一致性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/905061.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

A important person

When I saw this title,the first “person” that came to mind was my little sister, my puppy dog called LaiBao. I still remember the first day I saw her. My mom bought it on internet and the solder took her to us. She was too small at that time. She curled…

模型2汇率的数据预处理环节

数据来源:中国银行官网 数据项:货币名称【欧元】、汇率、时间 数据预处理围绕四个方面展开:重复值、异常值、归一化、缺失值 由于数据来源于官方网站,本身不存在缺失值及异常值,通过观察样本数据可知,数据的波动范围在7.800~8.000之间【数据保存三位小数】,波动范围较小…

SciTech-EECS-Circuits-AGC(Auto Gain Control, 自动增益控制)电路 的几种方式对比: 响应时问、精度、动态范围、线性度、稳定度

参考 https://www.elecfans.com/article/83/116/2010/20101201227060.html AGC 的几种方式"误差放大器" 的 AGC(自动增益控制): 放大量小了,稳压效果不好,放大量大了,容易自激。 AGC重点参数: 响应时问、精度、动态范围、线性度、稳定度.用“直流电压负反馈方式”…

昆明理工大学25冶金工程考研预计调剂169人

--冶金工程考研809冶金物理化学有色冶金学有色金属冶金冶金过程及设备F002钢铁冶金学冶金调剂

平衡树-入门

本文有一只奶龙编写,有借鉴会在其中说明。 平衡树 要了解平衡树是什么,我们需要先了解一个东西:二叉搜索树(也称二叉查找树)。二叉搜索/查找树(BST)如果我们有一颗二叉树可以进行查找,那么就可以说明其中的节点一定又有一个能够区分大小的“键值”。我们有节点来存储这…

安卓工程目录结构

根目录 根目录下有多个重要的文件和文件夹,各自承担着不同的功能。.gradle 和 .idea 这两个文件夹属于 IDE(集成开发环境)生成的文件,其中 .gradle 包含 Gradle 构建系统的配置和缓存信息;.idea 则是 IntelliJ IDEA 或者 Android Studio 所产生的项目配置文件。 app 这是安…

LLM大模型:post-train实战 - 使用GRPO微调LLM

deepseek带火了GRPO,更带火了reinforcement learning,让研究人员发现RL能在pre-train的基础上较大提升LLM的逻辑推理能力!当前,互联网高速发展二十多年产生的优质数据已经使用殆尽,所以更大规模的LLM一直难产(GPT-5现在都还没发布,优质token耗尽是核心原因之一)。市面上…

MQ 消息幂等性保证

MQ 消息幂等性保证 1. 什么是幂等性 在程序开发中,是指同一个业务,执行一次或多次对业务状态的影响是一致的。例如:根据 id 删除数据 查询数据在实际业务中,避免不了出现用户连续点击退款、重复点击删除等情况,这种情况下,就需要对多个消息进行处理,避免短时间内多次执行…

3.24 曲线/曲面积分

1 第一类曲线积分 (理解成求曲线的质量) 要把ds(弧微分)转化成dt(参数方程里面的自变量)(积分里面只留下的变量),也可以转化成dx什么的,注意ds转化成dx的公式 2 第一类曲面积分 还是先求投影,比如投影到xoy平面上,就求z=z(x,y) 2.1 普通对称性奇函数为0,偶函数*2 2.…

SmolVLM2: 让视频理解能力触手可及

一句话总结: SmolVLM 现已具备更强的视觉理解能力📺 SmolVLM2 标志着视频理解技术的根本性转变——从依赖海量计算资源的巨型模型,转向可在任何设备运行的轻量级模型。我们的目标很简单: 让视频理解技术从手机到服务器都能轻松部署。 我们同步发布三种规模的模型 (22 亿/5 亿…

React-Native开发鸿蒙NEXT-video

React-Native开发鸿蒙NEXT-video 前几周的开发,基本把一个”只读型“社区开发的差不多了。帖子列表,详情,搜索都迁移实现了,但还差了一点------视频类型帖子的展示。之前开发RN社区中,对于视频的处理用的是react-native-video,这个三方组件也已经实现了鸿蒙化,部分逻辑可…

React-Native开发鸿蒙NEXT-cookie设置

React-Native开发鸿蒙NEXT-cookie设置 应用有个积分商城,做一些积分兑换的业务,就一个基于react-native-webview开发的页面,在页面加载的时候通过js注入来设置cookie带入用户信息。 早先应甲方要求web网站关闭了,现在又要继续运行。于是就把web服务启动了,然后发现应用里积…