Java内存模型 (JMM)
基础概念
-
并发编程模型
- 并发编程需解决两大核心问题:线程间的通信与同步。
- 通信机制分为两类:共享内存和消息传递。
- Java采用的是共享内存并发模型,线程之间通过读写共享变量进行隐式通信。
-
Java内存模型的抽象
- JMM是控制Java线程间通信的规则,它决定了一个线程对共享变量的写入何时对另一个线程可见。
- 抽象结构:
- 主内存:存储所有线程共享的变量(实例域、静态域、数组元素)。
- 本地内存:每个线程私有的工作内存,存储了共享变量的副本。它是一个抽象概念,可以包括缓存、寄存器等。
- 线程间通信过程:线程A要将更新后的共享变量值传递给线程B,必须经过两个步骤:1. 线程A将本地内存的副本刷新到主内存。2. 线程B从主内存读取更新后的变量值。
核心问题:重排序
为提升性能,编译器和处理器会对指令进行重排序,这可能导致多线程程序出现内存可见性问题。
-
重排序的类型
- 编译器优化的重排序:不改变单线程语义的前提下,重新安排语句执行顺序。
- 指令级并行的重排序:处理器为了重叠执行多条指令而改变执行顺序。
- 内存系统的重排序:由于缓存和读写缓冲区的存在,使得内存的读写操作看起来是乱序的。
-
JMM的对策
- JMM通过定义一套规则,禁止特定类型的编译器和处理器重排序。
- 实现方式是在生成指令时,在适当位置插入内存屏障(Memory Barriers),以禁止特定类型的重排序。
核心原则:Happens-Before
JSR-133内存模型提出了Happens-Before的概念,用于阐述操作间的内存可见性。如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须存在Happens-Before关系。
-
主要规则
- 程序顺序规则:在一个线程内,书写在前面的操作happens-before于书写在后面的操作。
- 监视器锁规则:对一个锁的解锁(unlock)操作happens-before于后续对这个锁的加锁(lock)操作。
- volatile变量规则:对一个volatile变量的写操作happens-before于任意后续对这个volatile变量的读操作。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
-
重要理解
- 两个操作存在Happens-Before关系,不意味着前一个操作必须在后一个操作之前执行。
- JMM仅要求前一个操作的执行结果对后一个操作可见。如果重排序不影响这个可见性(和最终结果),JMM是允许的。
重排序的细节与影响
-
数据依赖性与as-if-serial语义
- 数据依赖性:如果两个操作访问同一个变量,且其中一个是写操作,则它们存在数据依赖。编译器和处理器不会对有数据依赖关系的操作进行重排序。
- as-if-serial语义:无论如何重排序,单线程程序的执行结果不能被改变。这保证了单线程程序的正确性。
-
重排序对多线程的影响
- 在多线程环境下,没有数据依赖关系的操作可能被重排序,从而破坏程序的原有逻辑。例如,线程A执行
a=1; flag=true;,可能被重排序为先执行flag=true,此时线程B可能在a=1还未执行时就读到了flag=true,导致逻辑错误。
- 在多线程环境下,没有数据依赖关系的操作可能被重排序,从而破坏程序的原有逻辑。例如,线程A执行
理论基础:顺序一致性模型
-
顺序一致性模型
- 一个理想化的理论参考模型,提供了极强的内存可见性保证。
- 两大特性:1. 单个线程内的所有操作都按程序顺序执行。2. 所有线程都只能看到一个单一、一致的操作执行顺序。
-
JMM与顺序一致性的关系
- 对于正确同步的多线程程序:JMM保证其执行结果与在顺序一致性模型中的执行结果相同。这是JMM为程序员提供的核心保障。
- 对于未同步或未正确同步的程序:JMM不保证其执行结果与顺序一致性模型中的结果一致。JMM只提供最小安全性保障,即线程读取到的值要么是之前某个线程写入的,要么是类型的默认值,不会凭空出现。
-
未同步程序的特性差异
- JMM不保证64位的long和double类型变量的读写是原子性的(在某些32位处理器上可能会被拆分为两次32位操作)。
JMM的设计哲学与实现
-
设计目标
- 在程序员(希望模型简单易懂,可见性强)和编译器/处理器(希望束缚少,优化空间大)之间找到平衡点。
- 核心原则:只要不改变正确同步程序的执行结果,就允许编译器和处理器进行优化。
-
实现机制
- JMM屏蔽了不同处理器内存模型的差异。常见的处理器内存模型通常比JMM要弱。
- Java编译器通过在适当位置插入不同类型的内存屏障指令,来确保在各种硬件平台上都能提供一致的内存可见性保证。
JSR-133对旧模型的修复
JDK5启用的JSR-133规范主要修复了旧内存模型的两个问题: 1. 增强了volatile的内存语义:严格限制volatile变量与普通变量的重排序。 2. 增强了final的内存语义:增加了重排序规则,保证了final变量的初始化安全性。