文章目录
引言
在多核处理器已成为标准配置的今天,并发编程是开发高性能应用程序不可或缺的能力。然而,并发编程也带来了巨大的复杂性,其核心挑战之一在于管理多线程对共享数据的访问。为了应对这一挑战,Java语言规范定义了Java内存模型(JMM)。
JMM并非一个物理存在的内存区域,而是一套抽象的规则和保证。它的主要目标是屏蔽各种硬件和操作系统的内存访问差异,确保Java程序在不同的平台上都能表现出一致的内存行为 。理解JMM是每一位Java并发程序员的必备技能,它直接关系到我们能否写出线程安全的代码。
第一部分:Java内存模型(JMM)核心概念
1.1 JMM的抽象结构:主内存与工作内存
为了规范线程间的通信,JMM定义了一个抽象的结构,包含 主内存(Main Memory) 和 工作内存(Working Memory) 。
- 主内存:是所有线程共享的区域,存储了Java中所有的实例字段、静态字段和构成数组对象的元素。在概念上,它主要对应于JVM运行时数据区中的Java堆和方法区 。
- 工作内存:是每个线程私有的数据区域。当线程需要操作主内存中的变量时,它会首先将变量的一个副本拷贝到自己的工作内存中。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量 。工作内存概念上涵盖了程序计数器、虚拟机栈和本地方法栈等线程私有区域 。
线程间的通信必须通过主内存来完成。例如,线程A若要将其对共享变量的修改传递给线程B,必须先将工作内存中更新后的值写回(write)到主内存,然后线程B再从主内存中读取(read)这个新值。
为了精确定义这种交互,JMM规定了8种原子操作 :
lock(锁定):作用于主内存的变量,把它标识为一条线程独占的状态。unlock(解锁):作用于主内存的变量,解除其锁定状态。read(读取):从主内存读取一个变量的值,以便后续的load操作使用。load(载入):把read操作从主内存中得到的变量值放入工作内存的变量副本中。use(使用):把工作内存中一个变量的值传递给执行引擎。assign(赋值):把一个从执行引擎接收到的值赋给工作内存的变量。store(存储):把工作内存中一个变量的值传送到主内存中,以便后续的write操作。write(写入):把store操作从工作内存中得到的变量的值放入主内存的变量中。
这些操作共同构成了JMM中变量从主内存到工作内存,再回到主内存的完整路径,是理解volatile等关键字背后机制的基础。
1.2 JMM的目标:并发编程的三大问题
JMM的设计旨在解决并发编程中普遍存在的三大核心问题:原子性、可见性和有序性 。
- 原子性(Atomicity) :指一个或多个操作,要么全部执行且执行过程不会被任何因素打断,要么就都不执行。JMM通过
lock和unlock这两个基本操作来保证大代码块的原子性,这些操作在Java中体现为synchronized关键字 。对于基本数据类型的读写,JMM也提供了原子性保证(除了非volatile的long和double在32位系统上可能被拆分为两次操作)。 - 可见性(Visibility) :指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。由于工作内存的存在,一个线程对共享变量的修改默认对其他线程是不可见的。JMM通过
volatile、synchronized和final关键字来提供可见性保证 。其底层机制是,在特定操作后,强制将工作内存的修改刷新到主内存,并在特定操作前,强制从主内存重新加载变量值。 - 有序性(Ordering) :指程序执行的顺序按照代码的先后顺序执行。然而,为了提高性能,编译器和处理器可能会对指令进行重排序(Reordering) 。JMM通过
volatile和synchronized来禁止特定类型的指令重排序,从而保证并发环境下的程序逻辑正确性 。
1.3 JMM与JVM内存区域的联系与区别
初学者常常将JMM与JVM的运行时数据区(JVM Memory Layout)混淆。必须明确,两者是不同层面的概念 。
- JVM运行时数据区:是JVM规范中定义的、在虚拟机运行时会使用到的不同内存区域。它包括线程共享的 堆(Heap) 和 方法区(Method Area,Java 8后为元空间Metaspace) ,以及线程私有的 Java虚拟机栈(VM Stack) 、 本地方法栈(Native Method Stack) 和 程序计数器(Program Counter Register) 。这是对内存的物理划分或逻辑划分。
- Java内存模型(JMM) :是一个抽象的概念模型,它并不真实存在。它描述的是一组规则或规范,用于控制程序中各个变量(实例字段、静态字段等)的访问方式 。
它们之间的映射关系是:
- JMM的主内存主要对应于JVM的堆和方法区。
- JMM的工作内存主要对应于JVM的虚拟机栈和本地方法栈等线程私有区域 。
简而言之,JVM内存布局描述了“数据存放在哪里”,而JMM描述了“数据如何被线程访问和共享”。
第二部分:JMM的核心同步机制与happens-before原则
2.1 指令重排序与内存屏障
在单线程环境下,为了不改变程序的执行结果,编译器和处理器可以自由地对指令进行重排序以优化性能,这被称为“as-if-serial”语义 。但在多线程环境中,这种重排序可能导致意想不到的后果,破坏程序的可见性和有序性 。
重排序示例(非volatile字段):
考虑以下代码,x, y, a, b均为非volatile的共享整型变量,初始值为0。
// 线程1
a = 1; // 操作1
x = b; // 操作2
// 线程2
b = 1; // 操作3
y = a; // 操作4
在没有同步的情况下,由于指令重排序,线程1的操作2可能被重排到操作1之前执行,线程2的操作4也可能被重排到操作3之前。如果发生这种情况,一种可能的结果是x=1且y=1,这在顺序执行的逻辑下是不可能出现的。更常见的一种重排序导致的结果是 x=0 且 y=0 。
为了解决这个问题,JMM引入了 内存屏障(Memory Barriers/Fences)。内存屏障是一种CPU指令,它有两个主要作用 :
- 禁止屏障两侧的指令重排序。
- 强制将屏障之前写入的数据刷新到主内存,或使屏障之后读取的数据从主内存加载,从而保证可见性。
JMM定义的内存屏障是抽象的,具体实现依赖于底层硬件 。Java开发者通常不直接使用内存屏障,而是通过volatile、synchronized等关键字间接触发它们的插入。
2.2 Happens-Before原则详解
happens-before原则是JMM中用于描述和判断操作之间内存可见性的核心工具。它定义了两个操作之间的偏序关系。如果操作A happens-before 操作B,那么JMM保证A操作的执行结果对B操作是可见的,并且A操作的执行顺序在B操作之前 。
JMM定义了以下几条天然的happens-before规则 :
- 程序顺序规则(Program Order Rule) :在一个线程内,按照程序代码顺序,书写在前面的操作
happens-before书写在后面的操作。 - 管程锁定规则(Monitor Lock Rule) :一个
unlock操作happens-before后续对同一个锁的lock操作 。 - volatile变量规则(Volatile Variable Rule) :对一个
volatile变量的写操作happens-before后续对这个变量的读操作 。 - 线程启动规则(Thread Start Rule) :Thread对象的
start()方法happens-before此线程的任何操作 。 - 线程终止规则(Thread Termination Rule) :线程中的所有操作都
happens-before对此线程的终止检测,例如通过Thread.join()方法结束、Thread.isAlive()的返回false。 - 线程中断规则(Thread Interruption Rule) :对线程
interrupt()方法的调用happens-before被中断线程的代码检测到中断事件的发生。 - 对象终结规则(Finalizer Rule) :一个对象的初始化完成(构造函数执行结束)
happens-before它的finalize()方法的开始。 - 传递性(Transitivity) :如果A
happens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeC 。
如果两个操作之间不存在happens-before关系,那么JMM不保证它们的执行顺序和可见性,虚拟机可以对它们进行任意重排序。
2.3 同步关键字的内存语义
volatile
volatile关键字是Java提供的最轻量级的同步机制。它确保了对volatile变量的读写操作具有以下内存语义 :
- 可见性:对一个
volatile变量的写操作会立即将此变量的新值刷新到主内存中。任何线程在读取该volatile变量之前,都会先从主内存刷新变量值,从而保证读取到的是最新值。这满足了volatile变量规则。 - 有序性:
volatile通过插入内存屏障来禁止指令重排序 。具体来说:- 当对
volatile变量进行写操作时,JIT编译器会在写操作前插入一个StoreStore屏障,在写操作后插入一个StoreLoad屏障。 - 当对
volatile变量进行读操作时,JIT编译器会在读操作后插入一个LoadLoad屏障和一个LoadStore屏障。
这确保了volatile变量的写操作不会被重排到其前面的任何操作之后,读操作不会被重排到其后面的任何操作之前。
- 当对
- 原子性:对任意单个
volatile变量的读/写操作是原子的。对于long和double类型的变量,volatile修饰符保证了其64位读写操作的原子性 。
synchronized
synchronized关键字提供了比volatile更强的同步能力,它通过管程(Monitor)机制实现。其内存语义体现在“管程锁定规则”上 :
- 当一个线程进入
synchronized代码块时,它会执行lock操作,这会导致该线程的工作内存失效,强制从主内存重新加载共享变量。 - 当一个线程退出
synchronized代码块时,它会执行unlock操作,这会将该线程工作内存中对共享变量的修改刷新到主内存。
因此,synchronized不仅保证了代码块的原子性,也同时保证了可见性和有序性。
final
final字段的内存语义主要用于保证 安全发布(Safe Publication) 。JMM为final字段提供了特殊的可见性保证 :
- 在一个对象的构造函数内,对一个
final字段的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能被重排序。 - 只要对象被正确地构造(即构造过程中
this引用没有逸出),那么一旦构造完成,其final字段的值对所有其他线程都是立即可见的,无需任何额外的同步措施 。
JSR-133(Java 5的JMM更新)加强了final的语义,确保了即使没有volatile或synchronized,不可变对象(其所有字段都是final)也能被安全地共享 。
第三部分:JMM的底层实现:硬件内存屏障
JMM的抽象规则最终需要通过具体的CPU指令来实现。HotSpot虚拟机在JIT编译时,会根据目标CPU架构的内存模型,生成相应的内存屏障指令 。
3.1 内存屏障的跨平台抽象
HotSpot内部定义了一套抽象的内存屏障接口,如OrderAccess::loadload(), OrderAccess::storestore(), OrderAccess::loadstore(), OrderAccess::storeload()等,分别对应四种类型的内存屏障 。JIT编译器在需要时调用这些接口,而这些接口的具体实现则会转换为特定平台的原生指令。
3.2 主流CPU架构下的具体实现
不同CPU架构的内存模型强度不同,导致其实现JMM所需屏障的方式也不同。
-
x86/x64架构:
- 内存模型:x86采用TSO(Total Store Order)强内存模型。它只允许“写-读”重排序(即Store-Load重排序)。写操作(Store)之间不会重排,读操作(Load)之间也不会重排。
volatile写:HotSpot在x86上实现volatile写时,通常会在对应的汇编指令后加上lock前缀,例如lock addl $0x0, (%rsp)。lock前缀本身是一个原子操作指令,它隐式地提供了一个StoreLoad屏障的完整效果,能够阻止其前后所有的读写操作重排序,并强制将缓存刷新到主存,代价较高但效果最强 。volatile读:由于x86的强内存模型,普通的读操作已经能保证有序性,因此volatile读通常不需要插入额外的硬件屏障 。- 锁操作:
lock和unlock通常也依赖于带有lock前缀的原子指令(如CMPXCHG)来实现。
-
ARM架构:
- 内存模型:ARM采用弱内存模型,允许几乎所有类型的读写操作重排序,因此需要显式地使用内存屏障指令。
volatile读写:在ARMv8架构上,HotSpot会优先使用更高效的ldar(Load-Acquire)和stlr(Store-Release)指令来实现volatile读和volatile写 。ldar确保其后的所有内存访问不会被重排到它之前,stlr确保其前的所有内存访问不会被重排到它之后。这些是ARMv8引入的单向屏障,比传统的双向屏障更高效 。- 在旧版ARM或特定情况下,可能会回退到使用
dmb(Data Memory Barrier)指令,这是一种更强的全功能屏障 。
-
PowerPC架构:
- 内存模型:PowerPC也是一种弱内存模型。
- 锁与原子操作:通常使用
lwarx(Load Word And Reserve Indexed)和stwcx.(Store Word Conditional Indexed)指令对来实现CAS(Compare-And-Swap)等原子操作,这是实现锁的基础 。 - 内存屏障:PowerPC提供了
lwsync(Lightweight Sync)和sync(Sync)等屏障指令 。lwsync保证在其前后的load/store指令的相对顺序,而sync是更强的屏障。在锁的实现中,通常会在lwarx/stwcx.循环前后配合使用isync或lwsync来确保正确的内存顺序 。
第四部分:JMM的演进与现代Java并发工具
4.1 JMM规范的稳定性与演进方向
一个重要的事实是,自Java 5(JSR-133)对JMM进行重大修订以来,其核心规范在后续的Java 8、11、17直至21等LTS版本中没有发生根本性的变化 。Java并发编程的基石——happens-before原则、volatile、synchronized、final的语义——都保持了稳定。
JMM的演进更多体现在形式化、兼容性和工具支持上。例如,JEP 188(Java Memory Model Update)项目旨在对JMM进行更严格的形式化定义,以更好地兼容C11/C++11内存模型,并为JVM上的其他语言提供支持 。
尽管核心规范稳定,但Java平台围绕JMM提供了更强大、更灵活的并发工具。
4.2 VarHandle的引入与内存顺序模式
Java 9引入的VarHandle API(JEP 193)是近年来JMM应用层面最重要的发展。它提供了一种标准的、类型安全的方式来访问对象的字段、数组元素以及堆外内存,旨在替代不安全的sun.misc.Unsafe类 。
VarHandle的核心优势在于它提供了对内存顺序的细粒度控制。它定义了一系列 访问模式(Access Modes) ,每种模式都有不同的内存排序保证 :
- 普通模式(Plain) :如
get、set。不提供任何额外的内存排序保证,类似于对普通变量的读写,可能会被重排序 。 - 不透明模式(Opaque) :如
getOpaque、setOpaque。保证操作的原子性,并禁止与该操作相关的重排序,但它不建立跨线程的happens-before关系。主要用于构建更复杂的同步原语 。 - 获取/释放模式(Acquire/Release) :如
getAcquire、setRelease。这对模式用于建立跨线程的happens-before关系。一个setRelease操作happens-before后续匹配的getAcquire操作 。这与C++11的memory_order_acquire/release语义兼容,比volatile更灵活,开销也可能更低。 - Volatile模式:如
getVolatile、setVolatile。提供与volatile关键字完全相同的内存语义,即保证可见性和完全的有序性 。
VarHandle还提供了显式的内存屏障方法,如fullFence()、acquireFence()、releaseFence(),让专家级用户可以像在C++中一样精确控制内存顺序 。
4.3 java.util.concurrent原子类的内存保证
java.util.concurrent.atomic包下的原子类(如AtomicInteger、AtomicReference)是无锁编程的关键。它们的实现依赖于底层的CAS操作,在早期JDK版本中依赖Unsafe,从Java 9开始,其内部实现逐渐迁移到使用VarHandle 。
这些原子类的方法提供了类似volatile的内存一致性保证 。例如,对一个AtomicInteger的set()操作,与后续对该实例的get()操作之间,存在happens-before关系 。compareAndSet()等方法的成功写入操作,同样与其之前的操作和后续的读取操作建立happens-before边,确保了操作的原子性和线程间的可见性。
4.4 虚拟线程(Project Loom)对JMM的影响
Java 19作为预览功能引入、并在Java 21中正式发布的虚拟线程,是一项革命性的并发调度技术。虚拟线程是用户态的轻量级线程,由JVM进行调度,而非操作系统内核 。
需要强调的是,虚拟线程的引入没有改变Java内存模型 。JMM是关于内存访问的规范,而虚拟线程是关于线程调度的实现。所有现有的同步原语,如synchronized、volatile、Lock以及VarHandle等,在虚拟线程上的行为和语义与在传统平台线程上完全一致 。开发者无需为虚拟线程学习新的内存同步规则。
4.5 外部内存访问API与JMM
自Java 17起正式引入的外部内存访问API(Foreign Memory-Function & Memory API),提供了一种安全、高效地访问堆外(off-heap)内存的机制 。该API的核心组件之一就是VarHandle。当需要访问由MemorySegment表示的外部内存时,可以通过MemoryHandles工厂创建VarHandle实例,然后使用其不同的访问模式来读写数据 。因此,对外部内存的访问同样遵循JMM的内存顺序保证,由所使用的VarHandle访问模式决定 。
第五部分:JMM与其他JVM机制的交互
5.1 JMM与垃圾回收(GC)
垃圾回收器(GC)在工作时,尤其是在并发GC(如G1, ZGC, Shenandoah)中,需要与应用程序线程协同工作。为了在不长时间暂停应用线程(Stop-The-World, STW)的情况下安全地移动对象和更新引用,现代GC广泛使用了 读屏障(Read Barrier) 和 写屏障(Write Barrier) 技术 。
- 写屏障:在应用程序对对象引用进行赋值时,JIT编译器会插入一小段代码。这段代码的作用是通知GC,某个对象的引用关系发生了变化。例如,在G1中,写屏障用于记录跨代引用的变化;在ZGC中,写屏障用于标记对象状态 。
- 读屏障:在应用程序读取对象引用时插入的代码。它的主要作用是在并发对象移动期间,确保应用程序线程能读取到对象的新地址 。
需要明确的是,GC的读写屏障与JMM中用于保证有序性的内存屏障是两种不同的技术,服务于不同的目的。GC屏障是为了GC算法的正确性,而JMM的内存屏障是为了并发编程中线程间的可见性和有序性。
5.2 JMM与JIT编译器
JIT(Just-In-Time)编译器是JMM规则的最终执行者。开发者编写的Java代码中的volatile、synchronized等关键字,会被JIT编译器识别。在将字节码编译为本地机器码时,JIT会分析代码上下文,并根据JMM的规范,在适当的位置插入平台相关的内存屏障指令 。JIT的优化(如锁消除、锁膨胀)也必须在不违反JMM保证的前提下进行。因此,JIT是连接JMM抽象规范和底层硬件现实的关键桥梁。
1190

被折叠的 条评论
为什么被折叠?



