定义
Java内存模型
Java内存模型是一套规范(JSR-133),屏蔽了底层不同硬件和不同操作系统之间的内存访问差异,定义了JVM在并发环境下变量如何与物理内存、CPU缓存交互从而进行读取和写入,以保证程序在不同平台上拥有一致的并发效果。
硬件背景
- 缓存一致性问题:现代CPU预算速度远快于内存,为了平衡速度差异,现代计算机在CPU和主存间加入了L1/L2/L3高速缓存,每个核心都有自己的L1/L2缓存,当多个核心同时读取并修改同一块主内存变量时,可能出现缓存不一致的问题。
- 指令重排序:编译器和处理器为了提高流水线运行效率,会对输入的指令进行乱序优化和重排序。
graph TD subgraph Core_1 ["CPU 核心 1"] Reg1["寄存器 (Registers)"] L1_I1["L1 指令缓存"] L1_D1["L1 数据缓存"] L2_1["L2 核心独占缓存"] Reg1 --- L1_D1 L1_I1 --- L2_1 L1_D1 --- L2_1 end subgraph Core_2 ["CPU 核心 2"] Reg2["寄存器 (Registers)"] L1_I2["L1 指令缓存"] L1_D2["L1 数据缓存"] L2_2["L2 核心独占缓存"] Reg2 --- L1_D2 L1_I2 --- L2_2 L1_D2 --- L2_2 end L3["L3 全局共享缓存"] L2_1 --- L3 L2_2 --- L3 Bus["系统总线"] RAM["主内存 (DRAM)"] L3 --- Bus Bus --- RAM
抽象模型
flowchart TD subgraph Threads ["线程工作区域"] T1["线程 A"] --> WM1["工作内存 A<br>(本地缓存 / 寄存器 / 写缓冲区)"] T2["线程 B"] --> WM2["工作内存 B<br>(本地缓存 / 寄存器 / 写缓冲区)"] end subgraph Memory ["物理内存区域"] MM["主内存<br>共享变量存储区 (堆 / 方法区)"] end WM1 <--> MM WM2 <--> MM
主内存:所有的共享变量(实例字段、静态字段、数组元素)都存储在主内存中。
工作内存:是一个抽象概念,涵盖了 CPU 寄存器、写缓冲器以及 L1/L2/L3 缓存,保存了线程使用的共享变量的主内存副本。
规定所有线程对变量的操作都必须在工作内存中进行,不同线程之间不能直接访问对方工作内存,传递变量需要通过主内存。
并发三大特性
原子性
一个或多个操作,要么全部执行成功且执行过程不被任何因素中断,要么全部都不执行。
实现:
- 基本数据类型(除了32位机器上的float、double)的读写是原子的。
synchronized和JUC的Lock等类提供块级别的操作原子性。- CAS与原子类保证复合操作的原子性。
可见性
当一个线程修改了某个共享变量的值,其他线程能够立即得知这个修改。
实现:
volatile:写操作时会强制刷新到主内存,读操作时需要从主内存重新加载。synchronized:解锁前必须把变量同步到主内存,加锁时工作内存中对应的变量失效,从主内存中重新加载。final:构造器完成初始化且没有发生this逃逸,其他线程无需同步即可看到正确的值。
有序性
程序执行的先后顺序按照代码顺序。
实现:
volatile:通过内存屏障禁用重排。
Happens-Before 原则
如果操作A happens-before 操作B,那么操作A的结果对操作B可见。
八大规则:
- 程序顺序规则:同一个线程中,前面的操作 happens-before 后面的操作。
- 监视器锁规则:对一个锁的解锁 happens-before 后续对同一个锁的加锁。
- volatile变量规则:对一个volatile变量的写操作 happens-before 后续对这个变量的读操作。
- 传递性:如果A happens-before B,B happens-before C,那么A happens-before C。
- 线程启动规则:
Thread.start()happens-before 该线程中的所有操作。 - 线程终止规则:线程中的所有操作 happens-before 其他线程检测到该线程已经终止(如
Thread.join()返回、isAlive()返回false)。 - 线程中断规则:对线程
interrupt()的调用 happens-before 被中断线程检测到中断事件。 - 终结器规则:一个对象构造函数的结束 happens-before 该对象
finalize()方法的开始。
volatile
可见性
- 写操作时:会将线程工作内存中所有共享变量刷新到主内存中。
- 读操作时:会将变量所在的缓存行置为无效,必须从主内存中重新读取。
JVM在生成汇编指令时,会在volatile变量写操作的指令前添加一个lock前缀(x86): - 将当前CPU核心的写缓冲区的数据立即刷新到主内存。
- 通过CPU的MESI缓存一致性协议和总线嗅探机制,向总线发送失效信号,使其他CPU核心缓存了该内存地址的缓存行失效。
有序性
JMM通过插入4种内存屏障来阻止指令重排序:
写操作:StoreStore + volatile 写 + StoreLoad
- StoreStore Barrier:禁止前面的写操作与后面的写操作重排。
- StoreLoad Barrier:禁止前面的写操作与后面的读操作重排,开销最大。
读操作:volatile 读 + LoadLoad + LoadStore - LoadLoad Barrier:禁止前面的读操作与后面的读操作重排。
- LoadStore Barrier:禁止前面的读操作与后面的写操作重排。
原子性
volatile不保证原子性。对于count++这类复合操作需要其他锁来保证原子性。
使用场景
- 状态变量:如
volatile boolean flag - 双重检查锁单例模式:双重检查锁
伪共享
CPU缓存行:CPU是以缓存行(通常为64字节)为单位加载的。
伪共享:两个独立的volatile共享变量放在同一个缓存行中,核心1写A变量会导致核心2中同一个缓存行的B变量失效,核心2中写B变量会导致核心1中同一个缓存行的A变量失效,导致频繁从主内存中重新加载,降低性能。
解决方法:
- 在变量前后填充7个long(字节),保证变量独占一个缓存行(如Disruptor)。
@Contended:JVM自动填充字节使其独占缓存行,需通过-XX:-RestrictContended启用(如LongAdder的Cell)。