news 2026/4/15 13:35:32

深入理解Java内存模型与volatile关键字:从理论到实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解Java内存模型与volatile关键字:从理论到实践

在多核处理器成为主流的今天,并发编程已成为每个Java程序员的必备技能。然而,编写正确的并发程序远比单线程程序复杂,主要原因在于我们需要处理两个核心问题:

线程之间如何通信?

线程之间如何同步?

Java内存模型(JMM)正是为了解决这些问题而设计的抽象概念。理解JMM不仅有助于编写正确的并发程序,还能帮助我们更好地利用现代硬件的性能优势。

2. Java内存模型的基础概念

2.1 并发编程的两个关键问题

通信机制:共享内存 vs 消息传递

/**

* 共享内存模型示例

* Java采用共享内存模型,线程通过读写共享变量进行隐式通信

*/

public class SharedMemoryExample {

private int sharedData = 0; // 共享变量

// 线程A通过写入共享变量与线程B通信

public void threadA() {

sharedData = 42; // 隐式通信:通过修改共享变量

}

// 线程B通过读取共享变量接收通信

public void threadB() {

if (sharedData == 42) {

System.out.println("收到线程A的消息");

}

}

}

现实比喻:把共享内存想象成公司的公告板

员工A在公告板上贴通知(写共享变量)

员工B查看公告板获取信息(读共享变量)

不需要直接对话,通过公告板间接通信

同步机制:显式 vs 隐式

/**

* Java需要显式同步

* 程序员必须明确指定哪些代码需要互斥执行

*/

public class ExplicitSynchronization {

private final Object lock = new Object();

private int counter = 0;

public void increment() {

synchronized(lock) { // 显式同步

counter++; // 临界区代码

}

}

}

2.2 JMM的抽象结构

三层存储架构

┌─────────────┐ ┌─────────────┐

│ 线程A │ │ 线程B │

│ │ │ │

│ 本地内存A │ │ 本地内存B │

│ ┌─────────┐ │ │ ┌─────────┐ │

│ │共享变量 │ │ │ │共享变量 │ │

│ │ 副本 │ │ │ │ 副本 │ │

│ └─────────┘ │ │ └─────────┘ │

└──────┬──────┘ └──────┬──────┘

│ │

└──────────────────┘

JMM控制交互

┌──────┴──────┐

│ 主内存 │

│ ┌─────────┐ │

│ │共享变量 │ │

│ └─────────┘ │

└─────────────┘

关键理解:

主内存:存储所有共享变量的"中央仓库"

本地内存:每个线程的"工作缓存",包含共享变量的副本

JMM:控制主内存与本地内存之间交互的"交通管理系统"

线程通信的详细过程

public class ThreadCommunication {

private int sharedVariable = 0;

public void demonstrateCommunication() {

// 初始状态:主内存和所有本地内存中 sharedVariable = 0

// 线程1执行:

sharedVariable = 100; // 1. 修改本地内存中的副本

// 2. 在某个时刻刷新到主内存

// 线程2执行:

int value = sharedVariable; // 3. 从主内存加载最新值

// 4. 存入本地内存副本

}

}

3. 重排序:看不见的性能优化

3.1 什么是重排序?

重排序是编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

public class ReorderingDemo {

private int a = 0, b = 0;

public void noReorder() {

// 程序员看到的顺序

a = 1; // 操作1

b = 2; // 操作2

int c = a + b; // 操作3

}

// 实际可能执行的顺序

public void actualExecution() {

b = 2; // 操作2先执行

a = 1; // 操作1后执行(重排序!)

int c = a + b; // 操作3:结果仍然是3

}

}

现实比喻:聪明的厨师优化做菜流程

新手厨师:严格按菜谱顺序,先烧水→再切菜→最后煮面(耗时8分钟)

资深厨师:先烧水→在等水开时切菜→水开了煮面(耗时5分钟,结果相同)

3.2 数据依赖性:重排序的底线

三种数据依赖类型

public class DataDependency {

// 1. 写后读 (Write After Read)

public void writeAfterRead() {

a = 1; // 写操作

b = a; // 读操作 ← 不能重排序!

}

// 2. 写后写 (Write After Write)

public void writeAfterWrite() {

a = 1; // 第一次写

a = 2; // 第二次写 ← 不能重排序!

}

// 3. 读后写 (Read After Write)

public void readAfterWrite() {

b = a; // 读操作

a = 1; // 写操作 ← 不能重排序!

}

}

数据依赖的现实意义

public class CookingDependencies {

public void makeSandwich() {

// 有依赖的操作(不能重排序):

bread = toast(); // 必须先烤面包

sandwich = putFilling(bread); // 才能放馅料

// 无依赖的操作(可以重排序):

prepareLettuce(); // 准备生菜

prepareTomato(); // 准备番茄 ← 顺序可以交换

}

}

3.3 as-if-serial语义:单线程的幻觉

as-if-serial语义:不管怎么重排序,单线程程序的执行结果不能被改变。

public class AsIfSerialExample {

public double calculateArea() {

double pi = 3.14; // 操作A

double r = 1.0; // 操作B

double area = pi * r * r; // 操作C

return area; // 总是返回3.14,无论A和B的执行顺序

}

}

数据依赖分析:

A → C(pi用于计算area)

B → C(r用于计算area)

A ↔ B(A和B没有依赖,可以重排序)

3.4 重排序对多线程的影响

问题代码示例

public class ReorderingProblem {

int a = 0;

boolean flag = false;

// 线程A执行

public void writer() {

a = 1; // 操作1:设置数据

flag = true; // 操作2:发布标志

}

// 线程B执行

public void reader() {

if (flag) { // 操作3:检查标志

int i = a * a; // 操作4:使用数据

System.out.println("结果: " + i);

}

}

}

两种危险的重排序

情况1:操作1和2重排序

// 期望顺序:a=1 → flag=true

// 重排序后:flag=true → a=1

// 结果:线程B可能看到flag=true但a=0

情况2:操作3和4重排序(猜测执行)

// 期望顺序:检查flag → 计算a*a

// 重排序后:提前计算a*a → 检查flag

// 结果:可能使用过期的a值进行计算

4. 顺序一致性:理想的内存模型

4.1 什么是顺序一致性?

顺序一致性是一个理论参考模型,为程序员提供极强的内存可见性保证。

public class SequentialConsistency {

// 两大特性:

// 1. 线程内顺序不变

public void perfectOrder() {

step1(); // 严格按顺序执行

step2(); // 严格按顺序执行

step3(); // 严格按顺序执行

}

// 2. 全局统一视图

public void globalView() {

// 所有线程看到相同的操作执行顺序

// 操作立即对所有线程可见

}

}

现实比喻:完美的电影放映系统

每个场景严格按剧本顺序播放

所有观众看到完全相同的画面

画面切换瞬间同步到所有观众

4.2 顺序一致性的工作机制

全局内存开关比喻

[线程1] [线程2] [线程3] ... [线程N]

↓ ↓ ↓ ↓

┌─────────────────────────┐

│ 全局内存开关 │ ← 像老式电话总机

└─────────────────────────┘

[全局内存]

工作方式:

1. 开关每次只连接一个线程到内存

2. 该线程执行一个完整操作

3. 然后开关切换到下一个线程

4. 所有操作串行执行,全局可见

4.3 JMM vs 顺序一致性

public class JMMvsSequential {

// 顺序一致性模型(理想):

class IdealWorld {

// - 单线程严格按程序顺序执行

// - 所有线程看到相同的操作顺序

// - 所有操作立即全局可见

// - 所有操作原子执行

}

// JMM现实模型:

class RealWorld {

// - 单线程内可能重排序(优化)

// - 不同线程可能看到不同的操作顺序

// - 写操作可能延迟可见(本地缓存)

// - long/double可能非原子操作

}

}

5. volatile的内存语义深度解析

5.1 volatile的基本特性

volatile与锁的等价性

public class VolatileEquivalence {

// 使用volatile的版本

class VolatileVersion {

volatile long counter = 0;

public void set(long value) {

counter = value; // 单个volatile写

}

public long get() {

return counter; // 单个volatile读

}

}

// 使用锁的等价版本

class LockVersion {

long counter = 0;

final Object lock = new Object();

public void set(long value) {

synchronized(lock) {

counter = value;

}

}

public long get() {

synchronized(lock) {

return counter;

}

}

}

}

关键理解:

单个volatile变量的读写 ≈ 用同一个锁同步的普通变量读写

但volatile++ ≠ 原子操作,需要额外同步

5.2 volatile的happens-before关系

经典的volatile通信模式

public class VolatileCommunication {

private int data = 0;

private volatile boolean ready = false; // volatile信号标志

// 生产者线程

public void producer() {

data = 42; // 1. 准备数据

ready = true; // 2. 发出信号(volatile写)

}

// 消费者线程

public void consumer() {

if (ready) { // 3. 检查信号(volatile读)

int result = data; // 4. 使用数据

System.out.println("结果: " + result); // 保证输出42

}

}

}

happens-before关系链

data = 42 → ready = true → if(ready) → result = data

↑ ↑ ↑ ↑

步骤1 步骤2 步骤3 步骤4

↓ ↓ ↓ ↓

程序顺序规则 volatile规则 程序顺序规则 传递性规则

关系推导:

1 happens-before 2(程序顺序规则)

2 happens-before 3(volatile规则:写先于读)

3 happens-before 4(程序顺序规则)

因此:1 happens-before 4(传递性)

结果:如果消费者看到ready=true,那么它一定能看到data=42

5.3 volatile的内存语义

volatile写:发送消息

public class MessageSending {

// volatile写就像发送广播消息:

public void sendMessage() {

prepareData(); // 准备消息内容

messageReady = true; // volatile写:广播发送

// 效果:所有准备的数据连带消息一起"发出"

}

}

volatile写的内存效果:

刷新线程本地内存中的所有共享变量到主内存

确保写操作之前的所有修改都对其他线程可见

volatile读:接收消息

public class MessageReceiving {

// volatile读就像打开收件箱:

public void receiveMessage() {

if (messageReady) { // volatile读:检查新消息

// 自动效果:清空本地缓存,重新加载所有数据

processData(); // 处理接收到的数据

}

}

}

volatile读的内存效果:

使线程的本地内存无效

强制从主内存重新加载所有共享变量

5.4 volatile内存语义的实现:内存屏障

内存屏障的作用

public class MemoryBarrierDemo {

private int x, y;

private volatile boolean flag;

public void writer() {

x = 1; // 普通写

y = 2; // 普通写

// StoreStore屏障:确保x=1, y=2先完成

flag = true; // volatile写

// StoreLoad屏障:确保flag=true立即可见

}

public void reader() {

// LoadLoad屏障:确保之前的读取完成

if (flag) { // volatile读

// LoadStore屏障:确保后续写入基于正确状态

int sum = x + y; // 保证看到x=1, y=2

}

}

}

四种内存屏障的详细解释

屏障类型 作用 现实比喻 插入位置

StoreStore 确保前面的写完成再执行后面的写 先炒完菜再装盘 volatile写之前

StoreLoad 确保前面的写完成再执行后面的读 先生产完产品再质量检查 volatile写之后

LoadLoad 确保前面的读完成再执行后面的读 先读完第一章再读第二章 volatile读之后

LoadStore 确保前面的读完成再执行后面的写 先诊断病情再开药方 volatile读之后

实际的屏障插入策略

public class ActualBarrierInsertion {

int a;

volatile int v1 = 1;

volatile int v2 = 2;

void readAndWrite() {

int i = v1; // volatile读

// LoadLoad屏障(可能被省略)

int j = v2; // volatile读

// LoadStore屏障

a = i + j; // 普通写

// StoreStore屏障

v1 = i + 1; // volatile写

// StoreStore屏障

v2 = j * 2; // volatile写

// StoreLoad屏障(必须保留)

}

}

优化原理:

编译器根据具体上下文省略不必要的屏障

但最后的StoreLoad屏障通常不能省略

不同处理器平台有不同优化策略

5.5 volatile的使用场景和限制

适合使用volatile的场景

public class GoodVolatileUse {

// 场景1:状态标志

private volatile boolean shutdownRequested = false;

public void shutdown() {

shutdownRequested = true;

}

public void doWork() {

while (!shutdownRequested) {

// 正常工作

}

System.out.println("程序已停止");

}

// 场景2:一次性安全发布

private volatile Resource resource;

public Resource getResource() {

if (resource == null) {

synchronized(this) {

if (resource == null) {

Resource temp = new Resource();

// volatile写确保对象完全构造后对其他线程可见

resource = temp;

}

}

}

return resource;

}

}

不适合使用volatile的场景

public class BadVolatileUse {

// 错误:volatile不能保证复合操作的原子性

private volatile int counter = 0;

public void unsafeIncrement() {

counter++; // 这不是原子操作!

// 实际包含:读 → 修改 → 写 三个步骤

// 多线程环境下可能丢失更新

}

// 正确做法:使用AtomicInteger或锁

private AtomicInteger safeCounter = new AtomicInteger(0);

public void safeIncrement() {

safeCounter.incrementAndGet(); // 原子操作

}

}

6. 扩展知识:MESI协议 - 硬件层面的缓存一致性

6.1 MESI协议基础:图书馆管理系统

基础概念映射

现实世界比喻:大型企业图书馆系统

─────────────────────────────────────

技术概念 ↔ 现实比喻

─────────────────────────────────────

CPU核心 ↔ 不同部门的会议室

缓存 ↔ 会议室里的白板

主内存 ↔ 中央档案室

缓存行 ↔ 白板上的一个主题区域

MESI状态 ↔ 白板的使用权限状态

总线 ↔ 公司内部广播系统

内存屏障 ↔ 强制同步会议

四种状态的现实意义

public class MESIStateMetaphors {

// Modified (M) - 已修改状态

// 比喻:你在会议室白板上做了独家修改,还没同步到中央档案室

// 特点:只有你有最新版本,别人看到的都是过时的

// Exclusive (E) - 独占状态

// 比喻:你借了档案室的资料,只有你的会议室有复印件

// 特点:你是唯一持有者,可以随时修改

// Shared (S) - 共享状态

// 比喻:多个会议室都有同一份资料的复印件

// 特点:大家看到的内容都一样,但不能直接修改

// Invalid (I) - 无效状态

// 比喻:你会议室的资料复印件已过期作废

// 特点:不能使用这份资料,需要重新获取

}

6.2 MESI协议完整状态转换的比喻场景

场景1:第一次获取资料(I → E)

技术场景:CPU第一次读取未缓存的数据

现实比喻:

市场部会议室(初始状态I):

1. 需要"年度销售报告"资料

2. 检查白板:没有这份资料

3. 通过广播系统:"谁有年度销售报告?"

4. 其他部门:都没回应(说明没人有副本)

5. 中央档案室:提供原始报告

6. 市场部:将报告贴到白板上,标记"独占(E)"

结果:只有市场部有这份报告,可以随时修改

场景2:共享阅读资料(E → S / I → S)

技术场景:第二个CPU读取同一数据

现实比喻:

技术部会议室(初始状态I):

1. 也需要"年度销售报告"

2. 检查白板:没有这份资料

3. 广播:"我也需要年度销售报告!"

市场部会议室(状态E)听到广播:

4. 回应:"我这里有,可以共享"

5. 将自己白板标记改为"共享(S)"

6. 提供复印件给技术部

技术部会议室:

7. 获得复印件,标记"共享(S)"

结果:两个部门都有相同报告,都不能单独修改

场景3:修改共享资料(S → M / S → I)

技术场景:CPU写入共享数据

现实比喻:

市场部会议室(状态S):

1. 发现报告有错误需要修改

2. 但不能直接修改(因为是共享状态)

3. 广播:"我要修改报告,请销毁你们的复印件!"

技术部会议室(状态S)听到广播:

4. 立即销毁复印件

5. 将白板标记改为"无效(I)"

6. 回应:"已销毁"

市场部会议室:

7. 收到所有确认后修改报告

8. 将标记改为"已修改(M)"

结果:只有市场部有最新版本,技术部的副本已作废

场景4:读取已修改资料(M → S / I → S)

技术场景:其他CPU读取被修改的数据

现实比喻:

技术部会议室(状态I):

1. 需要查看最新报告

2. 检查白板:标记为I(复印件已销毁)

3. 广播:"我需要最新的年度销售报告!"

市场部会议室(状态M)听到广播:

4. 将修改后的报告复印一份送到中央档案室更新

5. 提供最新复印件给技术部

6. 将自己标记改为"共享(S)"

技术部会议室:

7. 获得最新复印件,标记"共享(S)"

结果:两个部门又都有相同的最新报告

6.3 MESI协议与volatile的关系

volatile如何利用MESI协议

public class VolatileWithMESI {

private volatile boolean flag = false;

private int data = 0;

public void writer() {

data = 42;

// volatile写会触发:

// 1. 将缓存行状态改为M(Modified)

// 2. 发送无效化消息给其他CPU

// 3. 等待所有确认

// 4. 将数据刷新到主内存

flag = true;

}

public void reader() {

// volatile读会触发:

// 1. 检查缓存行状态,如果是I(Invalid)

// 2. 发送读请求到总线

// 3. 从主内存或其他CPU获取最新数据

if (flag) {

// 由于MESI协议,这里保证看到data=42

System.out.println(data);

}

}

}

MESI协议的消息类型比喻

public class MESIMessageMetaphors {

// 读请求 (Read)

// 比喻:"谁有XXX资料?借我看看"

// 目的:获取资料的只读副本

// 读无效化 (ReadInvalidate)

// 比喻:"我要修改XXX资料,请把你们的复印件都给我/销毁"

// 目的:获取独占修改权

// 无效化 (Invalidate)

// 比喻:"你们手里的XXX资料已过期,请立即销毁"

// 目的:使其他副本失效

// 写回 (WriteBack)

// 比喻:"我把修改后的资料送回中央档案室更新"

// 目的:将修改同步到主存储

// 响应 (Response)

// 比喻:"我这里有资料,给你复印件"

// 目的:提供数据给请求者

}

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/13 2:40:21

基于springboot + vue酒店管理系统(源码+数据库+文档)

酒店管理 目录 基于springboot vue酒店管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于springboot vue酒店管理系统 一、前言 博主介绍:✌️大…

作者头像 李华
网站建设 2026/4/14 21:02:37

25、Linux 下卡拉 OK 系统搭建与文件处理全解析

Linux 下卡拉 OK 系统搭建与文件处理全解析 1. TiMidity 运行与配置 在尝试使用标准包 TiMidity v2.13.2 - 40.1 运行接口时,程序在内存释放调用中崩溃。由于代码经过剥离,很难追踪崩溃原因,而且也不确定该包编译时所依赖的库和代码版本。 为了解决这个问题,可以从源代码…

作者头像 李华
网站建设 2026/4/15 4:23:45

非结构化数据的隐私性较低吗?

从听过任何关于人工智能讨论的调查来看,我们都知道隐私很重要。我们一次又一次地听到人们谈论如何实现某种类型的人工智能系统,但他们担心涉及的隐私问题。有时候,从整体格局的细致角度来看,能让我们看到如何做得更好。例如&#…

作者头像 李华
网站建设 2026/4/13 23:41:22

29、基于 Java Sound 的卡拉 OK 应用与字幕处理

基于 Java Sound 的卡拉 OK 应用与字幕处理 1. SequenceInformation 类 SequenceInformation 类是一个便利类,被多个其他类使用。它存储了序列、歌词行和旋律音符的副本,用于通过用户界面展示歌词和旋律,还存储了歌曲标题、设置音符显示范围的最大和最小音符,以及旋律所…

作者头像 李华
网站建设 2026/4/3 23:09:47

QMCDecode音频格式转换终极指南:Mac音乐解密完整教程

QMCDecode音频格式转换终极指南:Mac音乐解密完整教程 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac,qmc0,qmc3转mp3, mflac,mflac0等转flac),仅支持macOS,可自动识别到QQ音乐下载目录,默认转…

作者头像 李华