Java 内存模型(JMM)中 volatile 的作用与限制

news/2025/11/4 22:24:54/文章来源:https://www.cnblogs.com/jovic/p/19188591

volatile关键字的主要作用是限制JVM进行指令重排,保证变量对其他线程的可见性,可以避免创建对象才完成了一半就被其他线程引用到这个对象,保证创建完成后才对其他线程可见。

static class Value {int x; // 默认 0
    Value() {// 模拟耗时初始化(增加重排序/竞态被观察到的概率)try { Thread.sleep(1); } catch (InterruptedException ignored) {}x = 42; // 正常初始化写在最后
    }
}// 非安全写法:instance 非 volatile
static class HolderUnsafe {Value instance;void init() {instance = new Value(); // 可能在某些重排序下,引用先可见而字段尚未写入
    }
}
public static void main(String[] args) throws Exception {final HolderUnsafe h = new HolderUnsafe();h.init();
}上面这段代码解析成字节码后可能是
public static HolderUnsafe init();0: getstatic     #2   // Field instance:LUnsafeSingleton;3: ifnonnull     246: new           #3   // class HolderUnsafe9: dup 10: invokespecial #4   // Method "<init>":()V13: putstatic     #2   // Field instance:LUnsafeSingleton;16: goto          024: getstatic     #227: areturn// Value() 构造器
public Value();0: aload_01: invokespecial #1   // Method java/lang/Object."<init>":()V4: aload_05: bipush        427: putfield      #5   // Field value:I10: return
注:
invokespecial #Method java/lang/Object."<init>":()Vaload_0把局部变量表索引 0 的引用加载到操作数栈
bipush 42 把一个有符号的 byte 常量(-128..127)推送为 int 到栈顶
putfield      #Field x:I 给对象实例设置字段值(非静态字段)。

字节码顺序(new / invokespecial / putstatic)看起来先完整构造再赋值,但这是在 Java 层面的指令序列,实际上顺序可能不是这样。
JIT 编译器与处理器、缓存系统可以在运行时做优化与重排序;在没有 volatile/同步的情况下,JMM 允许构造器对字段的写操作相对于对静态引用的写操作在可见性上发生重排序(或可见性延迟),从而使其他线程看到非空引用但字段仍为默认值。

可能的执行时间线(示意,A 为写线程,B 为读线程):
线程 A (写入)
   [A1] 分配内存(object @0xABC)
   [A2] 在构造器中写入字段 value = 42(写入到 CPU 缓存 / 寄存器,尚未回写到主内存)
   [A3] 把引用写到 instance(putstatic),该写先被刷到主内存 / 对其它核心/线程可见
线程 B (读取,几乎同时)
   1.读取 instance,发现非空(看到了 A 的 [A3])
   2. 读取 instance.value,此时由于 A 的 [A2] 尚未对 B 可见或被重排序,读到默认值 0 -> 半初始化现象

解决办法:

把 instance 声明为 volatile 或在创建/发布时使用同步(synchronized),volatile 在写入时会产生必要的内存屏障,保证构造器对字段的写入对其它线程在读取 instance 后可见,从而避免半初始化。

内存屏障是用于控制可见性与操作顺序的低层机制,语言层(volatile、synchronized、原子类)封装了这些屏障以保证并发正确性。

 

Spring 中的常见应用
  延迟初始化/单例发布(lazy singleton / DCL):在自行实现延迟单例或发布代理/工厂引用时用 volatile 保证读到已初始化对象。
  发布缓存/引用替换:把构造完成的对象一次性赋给 volatile 字段以保证其它线程看到完整状态。
  生命周期/标志位:volatile boolean 做已初始化、已销毁、正在创建等状态标记以做快速无锁检查。
  读多写少的轻量可见性场景(替代不必要的同步)。
MyBatis 中的常见应用
  SqlSessionFactory 等延迟单例的 DCL(双重检查)字段用 volatile。
  Mapper 注册表 / 配置对象的引用替换发布(一次性构造并赋给 volatile 引用)。
  解析/加载标志(比如 parsed)防止重复解析(快速路径 + 同步块双重检查)。
  仅需可见性而非互斥时使用 volatile(降低同步开销)。

 

在 Spring/MyBatis 里,volatile 常用于发布已初始化的引用和状态标志,用以保证可见性与防止重排序;不使用时会引入半初始化、重复初始化或读取过期数据等难排查的并发错误。

MyBatis 启动时是否多线程?
   MyBatis 核心的 SqlSessionFactoryBuilder.build()/Configuration 构建在典型应用(手动或 Spring 启动)是单线程执行的。
    但在实际运行时有多种并发场景:Spring 多线程初始化 Bean(或插件/热加载)、多个线程并发首次访问懒加载的 Mapper、应用代码在运行期动态加载/解析 Mapper/资源等。这些场景会使解析逻辑可能被并发触发,因此需要线程安全的保护(volatile/同步/CAS)。ABA 问题指在线程使用比较并交换(CAS)检查某个变量时,变量从值 A 变为 B 又变回 A,CAS 只看值仍是 A,从而无法检测到中间发生了变化,可能导致错误的并发行为。

 

Spring bean的初始化是多线程的吗?

默认不是多线程的。
要点:
Spring 在容器刷新(ApplicationContext.refresh())期间会调用 DefaultListableBeanFactory.preInstantiateSingletons() 预先实例化单例,这个过程默认在启动线程单线程完成(避免循环依赖与复杂依赖图的问题)。
如果开启懒加载(例如 Spring Boot spring.main.lazy-initialization=true)或在运行时多个线程并发第一次访问某个懒加载 Bean,那么 Bean 的创建可能由多个线程触发;Spring 在 DefaultSingletonBeanRegistry.getSingleton(...) 等机制上做了同步/阻塞保证,通常只会最终产生单个实例(其他线程等待或使用已发布的引用),但并不是并行初始化。
如果应用代码或自定义后处理器显式创建线程并并行调用 beanFactory.getBean(...),就可能出现并发初始化,需要自己保证线程安全(或借助 Spring 提供的机制)。
总结:默认单线程初始化。只有在懒加载或应用层显式并发访问/并行化初始化逻辑时,才会出现多线程参与创建,且需注意同步、可见性与依赖顺序问题。

 

注意:

synchronized:提供互斥(mutual exclusion)和可见性。进入同步块时获取对象锁,退出时释放锁。
volatile:只保证可见性和有序性(内存屏障),不保证互斥。

Lock 接口(ReentrantLock 等)提供比 synchronized 更细粒度的控制(tryLock、可中断锁等),需在 finally 中释放锁。

原子类(java.util.concurrent.atomic.*,如 AtomicInteger/Long/Reference)使用 CAS 保证单变量的原子更新,性能高于加锁。原因是原子类使用 CPU 原子指令(CAS)实现无锁或自旋式更新,避免线程阻塞、上下文切换和内核调度开销,并在低争用时显著降低延迟与锁竞争开销。但在高争用或需要保证多个操作的复合原子性时,锁可能更合适;另有如 LongAdder 适合高并发累加场景。ABA 问题指在线程使用比较并交换(CAS)检查某个变量时,变量从值 A 变为 B 又变回 A,CAS 只看值仍是 A,从而无法检测到中间发生了变化,可能导致错误的并发行为。

 

什么时候用ThreadLocal 而不是加锁?

ThreadLocal 适合线程独占的状态/对象复用场景,能避免 synchronized 的锁竞争与阻塞,通常在性能敏感且每个线程需要独立副本时比 synchronized 更好。
如下列场景:
1.请求/会话级上下文保存:Web 请求链路中的 requestId、用户信息等,线程间不共享。
2.避免频繁创建/销毁代价大的对象,但又不能共享(线程内重用)。
3.高并发下对局部状态读写频繁,锁竞争成为瓶颈时。

4.MyBatis 会话/事务管理:MyBatis 的 SqlSession / 自定义会话持有器有时用 ThreadLocal 管理当前会话。

5.日志上下文(MDC):SLF4J/Logback 的 MDC 本质上基于 ThreadLocal 保存 traceId 等,方便日志关联。

 

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

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

相关文章

今日学习:二分

P3853 天津省选 /* 这个题和P2678几乎一样,但说实话我还没看懂。 1.首先检查的标准我没想到。是要检查当前的空旷指数还是路标数? 2.其次check的逻辑我还是没想明白。 3.犯了个小错。计算mid应该放在while循环里面。…

Ice Breaker Games - 一个在线免费的游戏网站,无需登录,打开即玩。

Ice Breaker Games - 一个在线免费的游戏网站,无需登录,打开即玩。 https://www.icebreakgame.com/

Java获取当前时间的下一天以及30天前的时间

有这样的一个需求。需要得到当前时间的下一天以及30前的时间。在计算30天前的时间时出现了一点问题,时间出错,但是公式没有出错,后来才发现是运算超出了Integer的范围。( 24 * 60 * 60*1000) 这种计算表达式在 Java…

论文导读:从 TSMC ISSCC 看 SRAM 存算发展

上次集中学习存算工作还是一年半以前,时光如梭,SRAM CIM 范式对比记忆又有新花样。本篇 blog 针对 ISSCC 2024 34.4 TSMC 的 3 nm 数字 SRAM 近存算 Macro 个例分析,并结合架构视角谈谈个人感触[1]。 RAM 的物理-逻…

edge chromium浏览器copilot图标消失处理

解决edge浏览器copilot图标消失找到edge浏览器的配置文件Local State %APPDATA%\..\Local\Microsoft\Edge\User Data修改配置variations_country 改成US,记得任务管理器完全关闭edge浏览器后修改

AI - 自然语言处理(NLP) - part 2 - 词向量 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

洛谷 P4577

题面太屎了。给定一棵大小为 \(n\) 的树,每个节点有权值 \(a_i\),问最多能选出多少个节点,使得若 \(v \in subtree_u\),则 \(a_v \ge a_u\) 成立。 \(n \le 2 \times 10^5\)。这个问题丢到序列上就是 \(LIS\) 了,…

C++算法贪心例题讲解 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

AI元人文:理论框架、僵局本质与文明演化的系统性构想

AI元人文:理论框架、僵局本质与文明演化的系统性构想 一、核心命题与理论基石核心命题:从“价值对齐”到“价值权衡”的范式革命传统AI的“价值对齐”范式陷入三重困境:认知科学误区(沿用过时的信息处理模型)、方…

[linux-mint] Surface Pro4 安装linux驱动

Surface Pro4 这个被淘汰下来的机器,一直没想好要如何处理,因为有平板,所以也不想刷Android系统,最终决定还是安装linux,当一个办公小平板来使用,顺便练练linux环境软件使用,所以就选择系统,然后发现好多linux…

[B] AGC VP 记录

AtCoder Grand Contest 049 AT_agc049_e [AGC049E] Increment Decrement AtCoder Grand Contest 052 约 1h 切 A,之后都不会了。 A - Long Common Subsequence 先从左到右放 \(n\) 个 \(0\),\(n\) 个 \(1\)。发现 \(…

2025年河南工业大学2025新生周赛(2)

A 小唐的签到 小唐到达教室的时间等于路上所用时间和上楼时间之和,注意如果教室在n楼,只需要上n-1层。#include<bits/stdc++.h> using namespace std; int main() {int a, x, n, b, y;cin >> a >> …

Atcoder [ARC161C] Dyed by Majority (Odd Tree) 题解 [ 绿 ] [ 树的遍历 ] [ 构造 ] [ 贪心 ]

Dyed by Majority (Odd Tree) 想起来无聊,写起来恶心。 首先手模一下,发现叶子节点可以确定它父亲的颜色。这启示我们自底向上确定颜色。 因此考虑在已确定所有儿子的颜色时,确定自己的颜色,此时有两种情况:儿子中…

Reflections on Trusting Trust by Ken Thompson

来源:https://aeb.win.tue.nl/linux/hh/thompson/trust.htmlKen Thompsons "cc hack" - Presented in the journal, Communication of the ACM, Vol. 27, No. 8, August 1984, in a paper entitled "R…

[Agent] ACE(Agentic Context Engineering)源码阅读笔记---(1)基础模块

[Agent] ACE(Agentic Context Engineering)源码阅读笔记---(1)基础模块 目录[Agent] ACE(Agentic Context Engineering)源码阅读笔记---(1)基础模块0x00 概要0x01 示例1.1 建立简单Agent1.2 后续操作Load and …

AI大语言模型从0开发

Transformer Tokenization 考虑到计算机没有办法直接识别人类语言,我们将每一个词映射为一个token使得计算机可以直接识别。 为实现这个目的我们使用BPE算法将每个词划分为若干个前缀和后缀,以此拼起来一个词,节省v…

第三十三篇

今天是11月4号,钳工实训真难。累死了。

2025.11.4

今天学习了二叉树的树和森林的转换

25.11.4 动态规划dp

1.返回值和状态更新写法 特征 举例 返回值是否有意义① 记忆化递归(自顶向下) 用返回值表示状态 dfs(i) 返回从 i 出发的最大值 ✅ 非常重要② 递推(自底向上) 不返回值,只更新表 dp[i][j] = ... ❌ 一般返回 voi…