Java 内存模型并发
基础概念
并发编程模型
- 共享内存模型:线程之间通过读-写公共的内存区域来进行隐式通信。Java采用此模型。
- 消息传递模型:线程之间没有公共状态,必须通过显式发送消息来通信。
JMM的抽象结构
JMM定义了线程和主内存(Main Memory)之间的关系。
- 主内存:存储所有线程共享的变量(实例域、静态域、数组元素等)。
- 本地内存(Local Memory):每个线程私有的工作内存,存储了该线程读/写共享变量的副本。本地内存是一个抽象概念,它涵盖了缓存、写缓冲区、寄存器等。
线程间的通信必须经过主内存。过程如下: 1. 线程A将更新后的共享变量值从其本地内存刷新到主内存。 2. 线程B从主内存读取线程A更新过的共享变量值到自己的本地内存。
重排序
为了提高性能,编译器和处理器会对指令进行重排序。
重排序的类型
- 编译器优化重排序:不改变单线程程序语义的前提下,重新安排语句的执行顺序。
- 指令级并行重排序:现代处理器采用指令级并行技术,如果不存在数据依赖性,可以改变指令的执行顺序。
- 内存系统重排序:由于缓存和写缓冲区的存在,使得加载和存储操作看起来是乱序执行的。
处理器重排序与内存屏障
- 写缓冲区:现代处理器使用写缓冲区来临时保存写入的数据,这可以提高性能,但也会导致处理器对内存的读/写顺序与实际发生的顺序不一致,尤其是在多处理器系统中,一个处理器的写缓冲对其他处理器是不可见的。这是导致内存可见性问题的根源之一。
- 内存屏障 (Memory Barriers):为了解决重排序带来的问题,JMM引入了内存屏障。编译器在生成指令序列时,会在适当位置插入内存屏障指令,以禁止特定类型的处理器重排序。
- LoadLoad Barriers
- StoreStore Barriers
- LoadStore Barriers
- StoreLoad Barriers (功能最全,开销也最大)
Happens-Before 原则
Happens-before是JMM中用于阐述操作之间内存可见性的核心概念。如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须存在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。
as-if-serial 语义
指无论如何重排序,(单线程)程序的执行结果不能被改变。编译器和处理器必须遵守此语义。为了遵守它,存在数据依赖关系的操作不会被重排序。但是,没有数据依赖关系的操作则可能被重排序。
重排序对多线程的影响
在单线程中,as-if-serial语义保证了程序的正确性。但在多线程环境中,即便是没有数据依赖的操作,其重排序也可能破坏程序的逻辑。例如,一个线程写入数据后再设置一个标志位,如果这两个操作被重排序,另一个线程可能会看到标志位被设置,但读取到的数据却是旧的。
顺序一致性模型
顺序一致性模型简介
这是一个理论上的参考模型,它为程序员提供了极强的内存可见性保证。
- 两大特性:
- 一个线程中的所有操作必须按照程序的顺序来执行。
- 所有线程都只能看到一个单一的操作执行顺序,每个操作都必须原子执行且立刻对所有线程可见。
JMM与顺序一致性模型的对比
-
对于正确同步的程序:JMM保证其执行结果与在顺序一致性模型中的执行结果相同。JMM允许在临界区内进行重排序以提高性能,但通过锁机制保证了其他线程无法观察到这种重排序,从而对外呈现出顺序一致性的效果。
-
对于未同步/未正确同步的程序:JMM提供的保证很弱。
- JMM不保证操作的执行顺序与程序顺序一致。
- JMM不保证所有线程看到的操作执行顺序是一致的。
- JMM不保证对64位
long和double类型变量的读写是原子性的(但在现代商用虚拟机中通常会实现原子性)。 - JMM仅提供最小安全性:线程读取到的值要么是之前某个线程写入的值,要么是类型的默认值(0, null, false),不会无中生有。
JMM的设计与总结
JMM、处理器模型与顺序一致性模型的关系
- 强度对比:顺序一致性模型 > JMM > 处理器内存模型。
- 越追求性能的处理器或语言,其内存模型就设计得越弱,对优化的限制就越少。JMM通过插入内存屏障,屏蔽了不同处理器内存模型的差异,为Java程序员提供了统一的、比大多数处理器模型更强的内存模型。
JMM的设计哲学
JMM的设计旨在平衡两个矛盾的目标:
- 程序员的需求:希望内存模型易于理解,提供强大的内存可见性保证(强模型)。
- 编译器和处理器的需求:希望内存模型的束缚尽可能少,以便进行更多优化来提升性能(弱模型)。
JMM的解决方案是:
- 对程序员:提供简单易懂的happens-before规则。
- 对编译器和处理器:遵循一个基本原则——只要不改变(单线程和正确同步的多线程)程序的执行结果,就可以进行任意优化。对于会改变程序执行结果的重排序,JMM要求必须禁止;对于不会改变的,则允许。
JSR-133对旧模型的修补
JDK 5之后采用的JSR-133内存模型主要做了两项重要的增强:
- 增强了volatile的内存语义:严格限制了volatile变量与普通变量的重排序,使其写-读操作具有与锁的释放-获取相同的内存语义。
- 增强了final的内存语义:保证了final域的初始化安全性,确保对象构造完成后,其他线程能看到final域的正确初始值。