网站首页 > java教程 正文
作为一名Java开发者,你是否曾对synchronized和volatile的深层原理感到困惑?是否在多线程编程中遭遇过难以捉摸的Bug?这一切的答案,都藏在Java Memory Model(JMM)之中。本文将带你拨开迷雾,真正理解JMM为何是并发编程的核心。
目录
- 引言
- 一、什么是Java内存模型(JMM)?
- 二、JMM的核心概念:主内存与工作内存
- 三、并发编程的三大难题:JMM要解决什么问题?
- 四、法宝之一:Happens-Before原则
- 五、法宝之二:内存屏障(Memory Barriers)
- 六、实战:代码中的JMM
- 总结与展望
- 互动环节
引言
在现代多核CPU时代,并发编程是提升应用性能的强大武器,但也引入了前所未有的复杂性。最令人头疼的问题往往不是业务逻辑,而是那些因“可见性”、“有序性”导致的诡异Bug,它们像幽灵一样时隐时现。
为了能让Java开发者屏蔽不同硬件内存模型的差异,在各个平台上都能编写出正确、高效的多线程程序,Java定义了自己的内存访问模型——Java Memory Model (JMM)。它不是真实存在的东西,而是一组规则和规范,定义了多线程环境下,对共享变量读写的访问机制。
一、什么是Java内存模型(JMM)?
简单来说,JMM是一套规则,它规定了多线程情况下,一个线程如何以及何时能看到另一个线程修改过的共享变量的值,以及如何同步地访问共享变量。
它的核心目标是解决由于CPU多级缓存、指令重排序等优化措施带来的并发问题,为开发者提供一套清晰的内存可见性保证。
二、JMM的核心概念:主内存与工作内存
JMM从逻辑上划分了两种内存空间,这是一种抽象概念,并不对等於实际的硬件内存:
- 主内存 (Main Memory): 存储所有共享变量。所有线程都能访问,但速度较慢。
- 工作内存 (Working Memory): 每个线程独有的一份“私有缓存”。它存储了该线程使用到的共享变量的副本。
线程对变量的所有操作(读、写)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方的工作内存。线程间的通信(传递变量值)必须通过主内存来完成。
交互流程简化版:
- Read(读取): 线程从主内存读取共享变量到自己的工作内存。
- Load(加载): 将read操作得到的值放入工作内存的变量副本中。
- Use(使用): 线程执行引擎使用工作内存中的变量值。
- Assign(赋值): 线程将新值赋给工作内存中的变量副本。
- Store(存储): 将工作内存中的变量值传送到主内存。
- Write(写入): 将store操作传来的值放入主内存的变量中。
这个过程中任何一步的延迟或乱序,都可能导致我们下面要讲的问题。
三、并发编程的三大难题:JMM要解决什么问题?
- 可见性 (Visibility)
- 问题:一个线程修改了共享变量的值,其他线程无法立即看到这个修改。
- 根源:修改发生在自己的工作内存,尚未刷新到主内存;或者其他线程的工作内存中还是旧的副本。
- 例子:线程A将flag改为true,但线程B却读到了旧的false,导致循环无法退出。
- 原子性 (Atomicity)
- 问题:一个或多个操作,要么全部执行成功,要么全部不执行,中间不能被任何其他操作中断。
- 根源:Java中的很多看似一步的操作(如i++),在底层其实是多个指令(读、改、写)。在多线程环境下,这些指令可能会被交错执行。
- 例子:两个线程同时对i++,最终结果可能只增加了1,而不是2。
- 有序性 (Ordering)
- 问题:程序代码的执行顺序不一定就是编译器和CPU实际执行的顺序。
- 根源:为了性能优化,编译器和处理器常常会对指令进行重排序。在单线程下,这没问题;但在多线程下,可能导致意想不到的结果。
四、法宝之一:Happens-Before原则
JMM为开发者提供了一套强大的逻辑工具——Happens-Before原则。它无需理解复杂的重排序和内存屏障细节,只需遵循这些规则,就能保证内存可见性。
规则释义:如果操作A Happens-Before 操作B,那么A操作的所有结果(包括对共享变量的修改)对B操作都是可见的。
JMM中一些天然的Happens-Before规则包括:
- 程序次序规则:在一个线程内,书写在前面的操作先行发生于后面的操作。
- 监视器锁规则:对一个锁的解锁操作 Happens-Before 于后续对这个锁的加锁操作。(这就是synchronized的可见性保证)
- volatile变量规则:对一个volatile变量的写操作 Happens-Before 于后续对这个变量的读操作。
- 线程启动规则:Thread.start() Happens-Before 于这个线程内的任何操作。
- 线程终止规则:线程中的所有操作都 Happens-Before 于其他线程检测到该线程已经终止(如Thread.join()返回)。
五、法宝之二:内存屏障(Memory Barriers)
Happens-Before是JMM提供给我们的“上层协议”,而底层实现则依赖于内存屏障(或称内存栅栏)。
你可以把它想象成一个栅栏,插入在两个CPU指令之间,禁止指令越过它进行重排序,并强制刷出CPU缓存数据。
volatile和synchronized等关键字的实现,底层都插入了各种类型的内存屏障(LoadLoad, StoreStore, LoadStore, StoreLoad),来保证特定的可见性和有序性。
六、实战:代码中的JMM
让我们用代码来感受一下可见性问题,以及如何使用volatile解决它。
public class JMMDemo {
// 尝试去掉 volatile 关键字,观察结果
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("Thread-A: Waiting for flag to be true...");
while (!flag) {
// 空循环,等待flag变为true
}
System.out.println("Thread-A: Success! Flag is now true.");
}, "Thread-A").start();
// 确保Thread-A先运行
Thread.sleep(1000);
new Thread(() -> {
System.out.println("Thread-B: Setting flag to true.");
flag = true;
}, "Thread-B").start();
}
}
代码解释:
- 两个线程共享变量flag,初始为false。
- Thread-A 在一个while循环中不断读取flag,直到它为true才退出。
- Thread-B 在1秒后将flag修改为true。
如果没有volatile:
- Thread-B修改了flag并写入自己的工作内存,但可能迟迟没有刷回主内存。
- Thread-A的工作内存中flag的副本一直是false,导致它永远看不到Thread-B的修改,陷入死循环。
有了volatile:
- 根据Happens-Before原则,Thread-B对flag的写操作 Happens-Before Thread-A对flag的读操作。
- volatile关键字底层插入的内存屏障,会强制将Thread-B工作内存中的修改立即刷写到主内存,并无效化其他线程中该变量的副本,迫使它们下次使用时必须重新从主内存读取。
- 因此,Thread-B修改后,Thread-A能立即看到新值,循环成功退出。
synchronized关键字通过加锁解锁机制,同样能保证原子性、可见性和有序性,但它是一种重量级的同步机制。
总结与展望
Java内存模型(JMM)是理解Java并发编程的基石。它抽象了主内存与工作内存的概念,定义了解决可见性、原子性和有序性三大难题的规范。通过Happens-Before原则和底层内存屏障的实现,volatile、synchronized、final等关键字得以协同工作,帮助开发者编写出线程安全的程序。
深入学习JMM后,你再去看java.util.concurrent包下的诸多强大工具(如ConcurrentHashMap, ReentrantLock, AtomicInteger等),就会发现它们无不是建立在JMM的规则之上。掌握了JMM,你才能真正地从“会用”并发工具,进阶到“知其所以然”,从而更自信地设计和调试复杂的多线程应用。
互动环节
你在Java并发编程中还遇到过哪些“灵异事件”?又是如何排查和解决的呢?欢迎在评论区分享你的经验和故事!
猜你喜欢
- 2025-10-14 看完这篇文,别再说你不懂Java内存模型了!
- 2025-10-14 Java volatile关键字深度解析:多线程编程的"内存屏障"神器
- 2025-10-14 Java内存模型JMM重要知识点_java内存模型有哪些
- 2025-10-14 Java 内存模型与并发编程中的可见性、原子性、有序性有啥关联
- 2025-10-14 让我们深入了解有关Java内存泄漏的10件事情
- 2025-10-14 Java中的volatile与操作系统的内存重排详解
- 2025-10-14 Java内存模型的历史变迁_java内存模型原理
- 2025-10-14 Kubernetes 下 Java 应用内存调优实战指南
- 2025-10-14 java使用NMT Native Memory Tracking分析内存占用
- 2025-10-14 【java面试100问】03 在生产环境上,发现内存泄漏问题,如何排查?
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)