尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

深入理解 Java 内存模型(JMM):从抽象规范到生产实践

深入理解 Java 内存模型(JMM):从抽象规范到生产实践 多线程编程是 Java 开发者的必修课而 JMM 则是理解并发安全的基石。本文将从核心定义出发系统梳理原子性、可见性、有序性三大特性深入剖析 volatile、指令重排序、Happens-Before 等关键机制并结合生产实践中的典型坑点帮助你建立完整的 JMM 知识体系。一、JMM 核心定义抽象而非物理Java 内存模型JMM是 Java 定义的抽象并发内存访问规范其核心目的是统一多线程环境下共享变量的读写规则屏蔽 CPU 缓存、操作系统内存架构等底层软硬件差异。重要前提JMM 不是真实的物理内存结构。1.1 两大抽象区域区域定义存储内容主内存抽象公共存储区线程共享变量类静态变量、对象实例成员变量、数组元素工作内存每个线程独占的抽象副本区域线程对共享变量的本地副本所有计算、读取、赋值在此完成线程无法直接读写主内存变量必须通过 8 个原子操作见第 5 节与工作内存交互再同步回主内存。1.2 实现层面的映射以 HotSpot 为例主内存→ 大致对应 Java 堆中存储的对象实例数据线程共享区工作内存→ 对应 CPU 寄存器、各级高速缓存、写缓冲区等硬件优化机制的抽象集合。它与 JVM 虚拟机栈的局部变量表、操作数栈等线程私有存储区有概念关联但并非直接等价1.3 局部变量的管控边界基础类型局部变量值本身存储在线程栈完全不受 JMM 管控引用类型局部变量引用本身是线程私有但引用指向的堆中对象的成员变量仍属于共享数据受 JMM 管控二、并发三大特性及底层内存语义并发编程的核心挑战可以归结为三大特性原子性、可见性、有序性。2.1 原子性操作不可中断定义一个或一组操作要么全部执行完毕要么完全不执行执行过程不可被中断。常见易错点i不是原子操作它分为「读取 i → 计算 1 → 赋值回 i」三步复合操作long/double的字撕裂问题JMM 规范允许非volatile修饰的 64 位变量拆分为两次 32 位读写HotSpot 64 位虚拟机已做原子性增强不存在字撕裂但 32 位 HotSpot 仍遵循规范允许字撕裂volatile修饰可强制 64 位整体读写原子性保障方案synchronizedLock显式锁java.util.concurrent.atomic原子类volatile 的原子性边界volatile仅保证单次读写的原子性核心价值是补齐 64 位long/double的原子读写a 10常量字面量赋值→ 原子操作a b读取其他变量后赋值→ 本质是「读 b 写 a」复合操作不具备原子性为什么 volatile 保证可见性却不能保证原子性可见性只保证读到的是最新值但无法阻止读-改-写过程中其他线程的插入。例如i场景线程 A 读到i5后在计算i1和写回的间隙线程 B 可能已经读到i5并完成了i6的写回最终线程 A 的写回会覆盖线程 B 的修改导致结果为 6 而非 7。2.2 可见性修改立即可见定义一个线程修改共享变量后其他线程能立即获取到该变量最新值。保障关键字及底层语义表格关键字底层语义volatile写操作触发工作内存副本刷新到主内存读操作失效当前线程缓存强制从主内存加载最新值synchronized/Lock锁释放等价volatile写刷新所有共享变量至主内存锁获取等价volatile读清空工作内存缓存从主内存重新加载finalJSR-133 增强禁止final字段的写操作重排序到构造方法之外禁止对象引用的读取与final字段的读取重排序final 可见性的破坏场景若对象引用在构造完成前被暴露会破坏final可见性保证构造方法内发生this逃逸如启动新线程、注册监听器传入this在构造方法中将对象注册到全局静态容器将对象作为参数传递给外部方法通过反射修改final字段的值JMM 不保证其可见性2.3 有序性禁止随意重排序定义禁止编译器、CPU 为优化效率随意指令重排序保证多线程环境执行逻辑符合编写代码语义。保障机制volatile内存屏障synchronizedLocksynchronized 的有序性边界synchronized仅禁止临界区内的指令重排序到锁范围之外。临界区内部的代码在不违反as-if-serial的前提下编译器/CPU 依然可以进行指令重排序。锁保证了临界区同一时间只有一个线程执行单线程内的重排序不会改变串行执行结果因此 JMM 允许临界区内重排序。补充synchronized同时具备原子性、可见性、有序性三大特性。三、指令重排序as-if-serial 的边界3.1 定义与分类编译器、CPU 处理器、内存系统在不违反as-if-serial前提下调整指令执行顺序以提升运行效率。三类重排序编译器重排序CPU 处理器重排序内存系统重排序as-if-serial是单线程的护身符但不是多线程的通行证。单线程内无论怎么重排序执行结果与代码顺序一致但多线程环境下一个线程的重排序可能被另一个线程观察到导致该线程看到违反直觉的执行顺序。3.2 三类数据依赖禁止单线程内重排表格依赖类型缩写示例说明写后读RAWa1; ba;读操作依赖前序写的结果写后写WAWa1; a2;后序写覆盖前序写的结果读后写WARab; b1;写操作依赖前序读的结果注意三类数据依赖仅在单线程内生效多线程间的数据依赖硬件无法感知不具备重排序约束效力。3.3 风险与 DCL 经典问题DCL双重检查锁漏洞根源instance new Singleton()可拆解为三步堆分配内存执行构造方法初始化对象将内存地址赋值给引用变量无volatile时可能重排为 ①→③→②其他线程会拿到非空但未初始化的半构造对象。volatile 修复原理volatile写之后插入的StoreLoad 屏障是核心禁止volatile写与后续的volatile读重排序保证对象初始化完成一定发生在引用赋值对其他线程可见之前其他线程读取volatile引用时LoadLoad和LoadStore屏障保证后续读取对象字段时能看到初始化完成的值。版本要求volatile修复 DCL 仅在JDK 1.5JSR-133及以上版本生效旧版 JMM 中volatile语义较弱无法禁止对象初始化与引用赋值的重排序。生产实践推荐更推荐静态内部类单例实现依托 JVM 类加载机制的clinit构造器同步锁天然符合 Happens-Before 规则既保证线程安全又避免手动编写 DCL 出错的风险。四、volatile 四大内存屏障底层实现volatile通过插入内存屏障限制跨指令重排是有序性保障的底层核心。屏障类型插入位置作用StoreStorevolatile写之前前面所有普通写不能重排到volatile写之后StoreLoadvolatile写之后禁止volatile写和后续所有读写重排开销最大是解决 DCL 问题的核心屏障LoadLoadvolatile读之后禁止volatile读和后续普通读重排LoadStorevolatile读之后禁止volatile读和后续普通写重排为什么 StoreLoad 屏障开销最大它需要确保屏障之前的所有写操作对屏障之后的所有读操作可见。在 x86 架构上这通常需要执行mfence指令或使用lock前缀指令会强制刷新写缓冲区、清空 CPU 流水线导致执行停顿。平台实现差异以上 4 类屏障是 JMM 保守插入策略的规范定义具体实现取决于底层硬件的内存模型x86强内存模型/TSO硬件天然禁止大多数重排序因此StoreStore、LoadLoad、LoadStore屏障在 x86 上都是空操作只有StoreLoad屏障会真实执行ARM/PowerPC弱内存模型更容易出现并发问题这也是 x86 环境下并发问题难复现、弱内存模型环境下容易出问题的核心原因是跨架构部署的典型坑点。生产性能坑伪共享False SharingCPU 缓存以缓存行Cache Line通常 64 字节为单位加载数据。当多个线程修改同一个缓存行内的不同共享变量时会导致缓存行频繁失效、来回刷新严重降低并发性能。典型场景Disruptor 环形队列中多个生产者线程竞争写入相邻的序列号槽位这些槽位大概率落在同一个缓存行内。解决方案手动字节填充PaddingJDK 8 使用Contended注解踩坑提醒JDK 8 中sun.misc.Contended、JDK 9 中jdk.internal.vm.annotation.Contended默认仅对 JDK 内置类生效。用户自定义类使用时必须添加 JVM 启动参数-XX:-RestrictContended否则注解不会生效。五、JMM 八大原子操作规范强制约束JMM 定义 8 个不可拆分的原子操作精确描述线程与主内存的交互过程以此为基础定义 Happens-Before 规则和内存可见性保证。这 8 个操作是 JMM 规范层面的最小原子单元实际 JVM 实现如 HotSpot不会严格按照这 8 个步骤执行而是通过内存屏障、缓存一致性协议等机制等效实现JMM 规范要求的语义。5.1 八大原子动作操作作用域说明lock主内存变量标记为线程独占状态执行 lock 会使该线程工作内存中对应变量的副本失效后续必须重新从主内存加载最新值unlock主内存变量释放独占锁read主内存 → 工作内存从主内存读取变量值load工作内存将read读到的值加载到线程工作内存生成副本use工作内存工作内存副本参与运算assign工作内存运算结果赋值给工作内存副本store工作内存 → 主内存工作内存变量传递到主内存write主内存将store的值写入主内存对应变量原理串联synchronized的加锁语义对应 JMM 的lock原子操作解锁语义对应unlock原子操作这是其能保证原子性、可见性的底层规范依据。5.2 JMM 强制约束read与load必须成对出现store与write必须成对出现不允许单独执行某一个操作但不要求严格连续执行中间可插入其他变量的操作如read a → read b → load a → load b合法lock和unlock成对出现同一变量同一时刻只能被一个线程lock同一线程可多次lock可重入最终unlock次数必须与lock次数相等工作内存assign修改后的变量最终必须执行storewrite同步回主内存不允许丢失修改不能跳过read直接在工作内存加载使用共享变量六、as-if-serial 语义生效范围仅限单线程内部规则无论编译器、CPU 如何重排序单线程最终执行结果和代码自上而下串行执行结果完全一致存在数据依赖的指令禁止重排序与 Happens-Before 的关系as-if-serial是 Happens-Before 程序顺序规则在单线程场景下的特例。单线程内程序顺序规则保证了前面的操作 HB 后面的操作与as-if-serial语义一致核心区别as-if-serial只保证最终结果正确不保证执行顺序与代码顺序一致Happens-Before 在此基础上进一步约束了跨线程的可见性区分as-if-serial管控单线程执行结果不负责多线程可见性Happens-Before 负责多线程跨线程内存可见性与重排序合法性判定。七、Happens-Before 先行发生原则JMM 判定跨线程变量可见性、指令重排序是否合法的核心准则如果操作 A Happens-Before 操作 B那么 A 所有修改对 B 全部可见JMM 禁止 A、B 之间破坏可见性的重排序。原理串联JMM 定义的内存屏障是 Happens-Before 规则在硬件层面的落地手段。例如volatile的内存屏障本质是通过禁止特定重排序来兑现volatile变量的 HB 可见性承诺。7.1 八条基础 HB 规则规则内容1. 程序顺序规则单线程内前面代码 HB 后面代码仅保证可见性因果关系不代表必须按书写顺序物理执行2. 监视器锁规则同一个锁的unlock操作 HB 后续任意lock操作3. volatile 变量规则volatile写 HB 后续对该变量任意volatile读4. 线程 start 规则Thread.start()调用 HB 子线程内全部操作5. 线程 join 规则子线程所有执行操作 HBjoin()方法返回6. 中断规则interrupt()调用 HB 线程检测中断状态的代码7. 对象终结规则构造方法执行完毕 HB 对象finalize()回收方法执行8. 传递性规则A HB BB HB C则 A HB C可见性可传递推导监视器锁规则应用示例// 线程A synchronized (lock) { sharedVar 1; // 操作Aunlock 之前的写操作 } // 操作Bunlock 操作 // 线程B synchronized (lock) { // 操作Clock 操作 int r sharedVar; // 操作D读取共享变量 }推导A HB B程序顺序规则→ B HB C监视器锁规则→ C HB D程序顺序规则→由传递性得 A HB D因此线程 B 一定能看到线程 A 对sharedVar的修改。7.2 核心认知Happens-Before 是逻辑上的可见性因果关系不等于物理时间上的先后顺序时间上先发生的操作不一定有 HB 关系比如线程 A 修改普通变量线程 B 随后读取该变量即使物理时间有先后也没有 HB 约束B 不一定能看到修改有 HB 关系的操作物理时间上不一定先执行JVM 可以在不破坏可见性保证的前提下重排指令Happens-Before 不保证的内容不保证物理时间上的先后执行顺序不保证没有 HB 关系的操作之间的可见性不保证原子性HB 只约束可见性和有序性不保证线程调度顺序HB 是内存可见性规则不是线程执行顺序规则两个操作不存在任何 HB 约束时JMM 可随意重排序多线程执行结果不可预知属于线程不安全。总结概念核心要点JMM抽象内存访问规范屏蔽底层差异主内存工作内存是逻辑概念原子性i非原子volatile仅保证单次读写原子性靠锁/原子类保障可见性volatile、synchronized、Lock、final均可保障HB 规则是判定依据有序性禁止随意重排序volatile通过内存屏障实现synchronized仅禁止跨临界区重排DCL必须用volatile或静态内部类注意 JDK 版本要求HB 规则8 条规则构成可见性推导体系是逻辑因果关系非物理时序理解 JMM 不是背诵规范条文而是建立抽象规范 → 底层实现 → 生产实践的完整认知链条。希望这篇文章能帮助你在并发编程的道路上少走弯路写出更健壮、更高效的代码。本文内容基于 JMM 规范及 HotSpot 实现整理如有疏漏欢迎指正交流。文章收录专栏Java 核心原理全解源码・并发・面试实战
返回列表