【深入剖析】攻克 Java 并发的基石:Java 内存模型 (JMM) 原理与实践指南

0.引言

理解 JMM (Java Memory Model - JMM) 是掌握 Java 并发编程的关键,它定义了多线程环境下,线程如何与主内存以及彼此之间交互内存数据。

核心目标: JMM 旨在解决多线程编程中的三个核心问题:

  1. 原子性 (Atomicity): 一个操作是不可中断的,要么全部执行成功,要么完全不执行。
  2. 可见性 (Visibility): 一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
  3. 有序性 (Ordering): 程序执行的顺序可能和代码编写的顺序不一致(指令重排序),JMM 定义了在何种情况下这种重排序是被允许或禁止的。

1.第一层:浅显易懂 - 为什么需要 JMM? (The Problem)

想象一下一个简单的场景:

public class VisibilityProblem {private static boolean flag = false; // 共享变量private static int value = 0;        // 共享变量public static void main(String[] args) {// 线程 Anew Thread(() -> {while (!flag) { // 循环检查 flag// 空循环,等待 flag 变为 true}System.out.println("Value: " + value); // 打印 value}).start();// 线程 Bnew Thread(() -> {value = 42;   // 步骤 1:设置 valueflag = true;  // 步骤 2:设置 flag}).start();}
}
  • 直觉期望: 线程 B 先设置 value = 42,然后设置 flag = true。线程 A 看到 flag 变为 true 后跳出循环,打印出 Value: 42
  • 现实问题:
    • 可见性问题: 线程 B 修改了 flagvalue,但这些修改可能只存在于线程 B 的 CPU 缓存或寄存器中,没有立即写回主内存。线程 A 在自己的缓存中看到的 flag 可能仍然是 false,导致它永远无法跳出循环。即使跳出了循环,它看到的 value 也可能是 0 而不是 42
    • 有序性问题: 编译器或处理器为了优化性能,可能会对指令进行重排序。线程 B 中的 flag = true 操作可能在 value = 42 之前执行。如果此时线程 A 看到了 flagtrue 而跳出循环,它看到的 value 就可能是未初始化的 0

JMM 就是为了解决这类在多线程环境下因缓存、指令重排序等优化带来的不可预测行为而制定的规则。


2.第二层:核心概念 - JMM 的抽象模型 (The Abstraction)

JMM 定义了一个抽象的内存模型,它屏蔽了底层硬件的具体实现细节(如 CPU 缓存、缓存一致性协议),为 Java 程序员提供了一个统一的视图:

  1. 主内存 (Main Memory):
    • 存储所有共享变量(实例字段、静态字段、数组元素)的原始值。
    • 是线程间共享的区域。
  2. 工作内存 (Working Memory / Local Memory):
    • 每个线程都有自己的工作内存。
    • 存储该线程使用到的共享变量的副本
    • 线程对共享变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存
    • 工作内存是 JMM 的一个抽象概念,它涵盖了 CPU 寄存器、各级缓存、写缓冲区等硬件优化。

内存交互操作 (8 种原子操作)

JMM 定义了 8 种原子操作来描述线程、工作内存和主内存之间的交互:

  • lock (锁定):作用于主内存变量,标识其为线程独占状态。
  • unlock (解锁):作用于主内存变量,释放锁定状态。
  • read (读取):作用于主内存变量,将变量值从主内存传输到线程的工作内存。
  • load (载入):作用于工作内存变量,将 read 操作得到的值放入工作内存的变量副本中。
  • use (使用):作用于工作内存变量,将变量值传递给执行引擎(如进行计算)。
  • assign (赋值):作用于工作内存变量,将执行引擎接收到的值赋给工作内存变量。
  • store (存储):作用于工作内存变量,将变量值从工作内存传输到主内存。
  • write (写入):作用于主内存变量,将 store 操作传输过来的值放入主内存变量中。

规则: JMM 规定这些操作必须满足特定的顺序和约束(如 readloadstorewrite 必须成对按顺序出现),但允许在成对操作之间插入其他操作(这是导致可见性和有序性问题的根源之一)。更关键的规则体现在 happens-before 原则上。


3.第三层:关键机制 - Happens-Before (HB)

happens-before 是 JMM 的核心概念,它定义了两个操作之间的偏序关系。如果操作 A happens-before 操作 B,那么:

  1. 可见性保证: A 对共享变量的修改(结果)一定对 B 可见。
  2. 有序性保证: A 在程序顺序上一定排在 B 之前执行(禁止某些重排序)。

注意: happens-before 并不一定意味着时间上的先后!它强调的是可见性和顺序的保证。如果两个操作之间没有 happens-before 关系,JVM 可以随意对它们进行重排序。

3.1JMM 定义的天然 Happens-Before 规则

  1. 程序次序规则 (Program Order Rule):同一个线程中,按照控制流顺序(可能是分支、循环等),前面的操作 happens-before 后面的操作。
  2. 管程锁定规则 (Monitor Lock Rule): 一个 unlock 操作 happens-before 于后续对同一个锁lock 操作。
  3. volatile 变量规则 (Volatile Variable Rule): 对一个 volatile 变量的写操作 happens-before 于后续对这个变量的读操作。
  4. 线程启动规则 (Thread Start Rule): Thread.start() 调用 happens-before 于被启动线程中的任何操作。
  5. 线程终止规则 (Thread Termination Rule): 线程中的所有操作都 happens-before 于其他线程检测到该线程已经终止(如 Thread.join() 返回成功或 Thread.isAlive() 返回 false)。
  6. 线程中断规则 (Thread Interruption Rule): 对线程 interrupt() 的调用 happens-before 于被中断线程检测到中断事件(抛出 InterruptedException 或调用 isInterrupted()/interrupted())。
  7. 对象终结规则 (Finalizer Rule): 一个对象的初始化完成(构造函数执行结束)happens-before 于它的 finalize() 方法的开始。
  8. 传递性 (Transitivity): 如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

happens-before 的意义: 程序员只需要利用这些规则(主要是通过 synchronizedvolatilefinal 等关键字以及 java.util.concurrent 包中的工具),就能确保多线程操作的可见性和有序性,无需关心底层复杂的缓存和重排序细节。


4.第四层:深入剖析 - volatile 关键字

volatile 是 JMM 中最重要的关键字之一,它提供了比 synchronized 更轻量级的同步机制。

4.1volatile 的语义

  1. 保证可见性:
    • 对一个 volatile 变量的写操作,会立即刷新到主内存
    • 对一个 volatile 变量的读操作,会从主内存中读取最新的值(或保证看到最近写入的值)。
    • 这通过禁止编译器/处理器对 volatile 变量的读写操作进行缓存优化来实现。
  2. 禁止指令重排序 (部分):
    • 编译器在生成字节码时,会在 volatile 写操作前后插入写屏障 (StoreStore + StoreLoad)
    • volatile 读操作前后插入读屏障 (LoadLoad + LoadStore)
    • 写屏障 (Store Barrier):
      • StoreStore: 确保在该屏障之前的所有普通写操作都刷新到主内存(对其他线程可见)。
      • StoreLoad: 确保在该屏障之前volatile 写操作都完成,并且刷新到主内存后,才能执行该屏障之后volatile 读/写操作(开销较大,通常由 StoreLoad 承担)。
    • 读屏障 (Load Barrier):
      • LoadLoad: 确保在该屏障之后的所有操作(普通读或 volatile 读)都在该屏障之后的读操作之前执行(禁止重排序),并且强制从主内存或最新缓存中加载数据。
      • LoadStore: 确保在该屏障之后的所有操作都在该屏障之后的写操作之前执行(禁止重排序),并且这些写操作的数据依赖在该屏障之前的读操作已完成。
    • 这些屏障共同作用,确保了 volatile 变量读写操作相对于其前后代码的相对顺序,从而实现了 happens-before 规则中的有序性保证。

4.2volatile 的局限性

  • 不保证原子性: volatile 不能保证复合操作的原子性。例如 volatile int count; count++; 这个操作 (count++ 包含读-改-写三步) 在多线程下仍然是不安全的。需要使用 synchronizedAtomicInteger 等。

4.3volatile 的典型用法

  • 状态标志位: 如开头的例子,用 volatile boolean flag; 来安全地通知其他线程状态改变。
  • 一次性安全发布 (One-Time Safe Publication): 利用 volatile 写操作的 StoreStore 屏障,确保在发布对象引用之前,对象的初始化已经完全完成(构造函数结束)。
    public class Singleton {private static volatile Singleton instance; // volatile 保证安全发布private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次检查 (无锁)synchronized (Singleton.class) {if (instance == null) { // 第二次检查 (加锁)instance = new Singleton(); // volatile 写}}}return instance;}
    }
    
    如果没有 volatile,其他线程可能看到一个未初始化完成Singleton 对象(指令重排序导致引用赋值在构造函数完成之前发生)。
  • 独立观察 (Independent Observation): 定期发布观察结果供其他程序使用。
  • 开销较低的读-写锁策略: 当读远多于写时,可以用 volatile 保证写操作的可见性,读操作不需要加锁(但写操作需要额外的同步机制如 synchronized 或 CAS 来保证原子性)。

5.第五层:JMM 与并发编程实践

理解 JMM 对于编写正确、高效、可预测的并发程序至关重要:

  1. 优先使用高级并发工具: java.util.concurrent 包 (ConcurrentHashMap, ExecutorService, CountDownLatch, CyclicBarrier, AtomicXxx 类等) 是构建在 JMM 基础之上的,它们通常比直接使用 synchronizedvolatile 更安全、更高效、更易用。理解 JMM 能让你更好地理解和使用这些工具。
  2. 正确使用 synchronized: synchronized 块不仅提供互斥(原子性),也提供强大的内存语义:进入 synchronized 块(获得锁)相当于执行一个 volatile 操作(能看到之前持有该锁的线程的所有修改),退出 synchronized 块(释放锁)相当于执行一个 volatile 操作(将修改刷新到主内存)。这确保了临界区内外操作的可见性和有序性。
  3. 理解 final 字段:final 修饰的字段在构造函数中初始化后,其值对其他线程是可见的(无需同步),前提是对象的引用本身是正确发布的(例如通过 volatilesynchronized 安全发布规则)。这是 JMM 对 final 的特殊保证。
  4. 避免过度同步: 不必要的同步会带来性能开销。理解 JMM 可以帮助你判断何时需要同步(主要是为了保护共享可变状态),何时可以避免。
  5. 警惕内存可见性导致的微妙 Bug: 很多并发 Bug 不是由竞态条件引起的,而是由内存可见性问题引起的。理解 JMM 是诊断这类 Bug 的基础。

6.总结

  • JMM 是什么? 一个规范,定义了多线程环境下 Java 程序如何与内存交互,确保程序在并发执行时的可见性有序性(以及通过其他机制如锁保证的原子性)。
  • 核心问题: 解决因 CPU 缓存、指令重排序导致的可见性有序性问题。
  • 抽象模型: 主内存(共享)和工作内存(线程私有副本),定义了 8 种内存交互操作。
  • 核心机制: happens-before 关系。它定义了操作间的可见性和顺序保证。JMM 定义了一系列天然规则(程序次序、锁、volatile、线程启动/终止等)。
  • volatile 关键字:
    • 保证可见性(写立即刷新,读获取最新值)。
    • 通过内存屏障StoreStore, StoreLoad, LoadLoad, LoadStore禁止特定类型的指令重排序
    • 不保证原子性
  • 实践意义: 理解 JMM 是编写正确、高效并发 Java 程序的基础。它解释了高级并发工具的工作原理,指导你正确使用 synchronizedvolatilefinal,并帮助你诊断复杂的并发 Bug。

掌握 JMM 需要时间和实践。从理解基本问题和抽象模型开始,逐步深入到 happens-beforevolatile 的细节,最终将其应用于并发编程实践中,是学习 JMM 的有效路径。

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

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

相关文章

【Three.js】初识 Three.js

Threejs介绍 我们开发 webgl 主要是使用 threejs 这个库,因为 webGL太难用,太复杂!但是现代浏览器都支持WebGL,这样我们就不必使用Flash、Java等插件就能在浏览器中创建三维图形。 threejs 它提供-一个很简单的关于WebGL特性的J…

【经验总结】ECU休眠后连续发送NM报文3S后ECU网络才被唤醒问题分析

目录 前言 正文 1.问题描述 2.问题分析 3.验证猜想 4.总结 前言 ECU的上下电/休眠唤醒在ECU开发设计过程中最容易出问题且都为严重问题,最近在项目开发过程中遇到ECU休眠状态下连续发送NM报文3S后才能唤醒CAN网络的问题,解决问题比较顺利,但分析过程中涉及到的网络休…

企业架构框架深入解析:TOGAF、Zachman Framework、FEAF与Gartner EA Framework

执行摘要 企业架构(EA)是一项至关重要的实践,它使组织能够协调其业务战略、运营流程和技术基础设施,以实现整体战略目标。企业架构框架作为结构化的方法论和综合性工具,旨在管理企业级系统的固有复杂性,提…

数字化动态ID随机水印和ID跑马灯实现教育视频防录屏

摘要:数字化动态ID随机水印和ID跑马灯技术可以有效保护数字教育资源。动态水印将用户信息随机显示在视频上且不可去除,能追踪录屏者并震慑盗版行为。ID跑马灯则自定义显示观看者信息,便于追踪盗版源头并提供法律证据。这些技术大幅增加盗版成…

< 自用文儿 腾讯云 VPS > Ubuntu 24 系统,基本设置

前言: 3 月份买的腾讯云的这台 VPS,刚发现现在退款,只能返回 0 元。测试应用已经迁移到JD,清除内容太麻烦,重装更简单。 因为配合政策,国内的云主机都有两个 IP 地址,一个内网,一个…

React ajax中的跨域以及代理服务器

Axios的诞生 为什么会诞生Axios?说到Axios我们就不得不说下Ajax。最初的旧浏览器页面在向服务器请求数据时,由于返回的是整个页面的数据,所以整个页面都会强制刷新一下,这对于用户来讲并不是很友好。因为当我们只需要请求部分数据…

HOT 100 | 73.矩阵置零、54.螺旋矩阵、48.旋转图像

一、73. 矩阵置零 73. 矩阵置零 - 力扣(LeetCode) 1. 解题思路 1. 使用两个数组分别标记每行每列是否有0,初始化全为False,遇到0就变成True。 2. 遍历矩阵,遇到0就将False改成True。 3. 再次遍历矩阵,更…

神经网络压缩

网络压缩技术学习笔记 以下笔记基于提供的 PDF 文件(tiny_v7.pdf),总结了网络压缩技术的核心概念、实现原理和方法,特别针对多模态大模型、空间智能以及未来智能体(Agent)和通用人工智能(AGI&a…

论索引影响性能的一面④ 索引失踪之谜【上】

梁敬彬梁敬弘兄弟出品 往期回顾 论索引影响性能的一面①索引的各种开销 论索引影响性能的一面②索引的使用失效 论索引影响性能的一面③ 索引开销与经典案例 开篇:DBA的深夜“寻人启事” 作为数据库的守护者,我们最信赖的伙伴莫过于“索引”。它如同一…

java集合(九) ---- Stack 类

目录 九、Stack 类 9.1 位置 9.2 特点 9.3 栈 9.4 构造方法 9.5 常用方法 9.6 注意点:循环遍历 Stack 类 九、Stack 类 9.1 位置 Stack 类位于 java.util 包下 9.2 特点 Stack 类是 Vector 类的子类Stack 类对标于数据结构中的栈结构 9.3 栈 定义&…

ARXML可视化转换工具使用说明

ARXML可视化转换工具 | 详细使用指南与说明 📝 前言 自上篇文章《聊聊ARXML解析工具:我们是如何摆脱昂贵商业软件的》发布以来,收到了众多朋友的关注和咨询,这让我倍感荣幸! 新朋友请注意:如果您还没有阅…

松胜与奥佳华按摩椅:普惠科技与医疗级体验的碰撞

在智能健康设备快速普及的今天,按摩椅已从奢侈品转变为家庭健康管理的重要工具。面对市场上琳琅满目的品牌,松胜与奥佳华凭借截然不同的发展路径,各自开辟出特色鲜明的赛道:前者以“技术普惠”理念打破高端按摩椅的价格壁垒&#…

一起学习Web 后端——PHP(二):深入理解字符与函数的使用

一、前言 在上一讲中,我们主要讲PHP的相关知识。本节我们将继续深入,学习: PHP 中各种语法字符、符号的含义与用法; PHP 中常用函数的种类、定义方式与实际应用。 这些知识是构建 Web 后端逻辑的基础,对于后期编写…

【Bluedroid】蓝牙启动之 GAP_Init 流程源码解析

蓝牙 GAP(通用访问配置文件)模块是蓝牙协议栈的核心组件,负责设备发现、连接管理及基础属性暴露等关键功能。本文围绕 Android蓝牙协议栈 GAP 模块的初始化流程与连接管理实现展开,结合代码解析其核心函数(GAP_Init、gap_conn_init、gap_attr_db_init)的功能逻辑,以及关…

最新四六级写作好词好句锦囊(持续更新中)

完整版四六级备考攻略可见另一篇博客~~(喜欢的留个点赞收藏再走呗~~) ​​​​​​四六级备考攻略-CSDN博客 一、通用 1、词组 2、单词 3、句型 二、老龄化、老年人 三、学习、社交、社会实践 四、文化、习俗 五、数字素养、数字技能 六、资…

Java 通用实体验证框架:从业务需求到工程化实践【生产级 - 适用于订单合并前置校验】

Java 通用实体验证框架:从业务需求到工程化实践【适用于订单合并前置校验】 一、业务验证痛点与需求背景 1. 传统验证方式的困境 传统验证方式存在代码冗余、维护成本高和扩展性差等问题。相同的验证逻辑在不同模块重复编写,修改验证规则时需要同步修…

PyArk飘云阁出品的ARK工具

PyArk是由飘云阁(PiaoYunGe)开发的一款功能强大的系统安全分析工具,主要用于Windows环境下的内核级检测与分析。该工具集成了进程管理、驱动模块扫描、内核及应用层钩子检测、进程注入等核心功能,旨在帮助安全研究人员深入识别潜在…

【高中数学之复数】已知复数z的幅角为60°,且|z-1|是|z|和|z-2|的等比中项,求|z|?(2003高考数学全国卷,解答题首题,总第17题)

【问题】 已知复数z的幅角为60,且|z-1|是|z|和|z-2|的等比中项,求|z|? 【来源】 2003高考数学全国卷,解答题首题,总第17题。 【解答】 解: 由复数辐长辐角定义有 zr*(Cos60iSin60) 据等比中项定义有&#xff1…

观点 | 科技企业到了品牌建设的历史性窗口期

随着全球科技产业的飞速发展,科技型企业作为推动技术创新和经济发展的重要力量,正面临着前所未有的机遇与挑战。近年来,中国科技行业保持了快速增长的态势。根据国家统计局的数据,2023年全国研究与试验发展(R&D&am…

影像组学5:Radiomics Score的计算

Rad-score(全称 Radiomics score,影像组学评分)是通过数学模型将影像组学提取的多个特征整合为一个综合性指标,从而简化临床分析与决策。 前文已介绍影像组学的病灶分割、特征提取及筛选流程,本节将重点阐述 Rad-scor…