定义


Java内存模型

Java内存模型是一套规范(JSR-133),屏蔽了底层不同硬件和不同操作系统之间的内存访问差异,定义了JVM在并发环境下变量如何与物理内存、CPU缓存交互从而进行读取和写入,以保证程序在不同平台上拥有一致的并发效果。

硬件背景


  1. 缓存一致性问题:现代CPU预算速度远快于内存,为了平衡速度差异,现代计算机在CPU和主存间加入了L1/L2/L3高速缓存,每个核心都有自己的L1/L2缓存,当多个核心同时读取并修改同一块主内存变量时,可能出现缓存不一致的问题。
  2. 指令重排序:编译器和处理器为了提高流水线运行效率,会对输入的指令进行乱序优化和重排序。
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可见。
八大规则:

  1. 程序顺序规则:同一个线程中,前面的操作 happens-before 后面的操作。
  2. 监视器锁规则:对一个锁的解锁 happens-before 后续对同一个锁的加锁。
  3. volatile变量规则:对一个volatile变量的写操作 happens-before 后续对这个变量的读操作。
  4. 传递性:如果A happens-before B,B happens-before C,那么A happens-before C。
  5. 线程启动规则:Thread.start() happens-before 该线程中的所有操作。
  6. 线程终止规则:线程中的所有操作 happens-before 其他线程检测到该线程已经终止(如Thread.join()返回、isAlive()返回false)。
  7. 线程中断规则:对线程interrupt()的调用 happens-before 被中断线程检测到中断事件。
  8. 终结器规则:一个对象构造函数的结束 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变量失效,导致频繁从主内存中重新加载,降低性能。
解决方法:

  1. 在变量前后填充7个long(字节),保证变量独占一个缓存行(如Disruptor)。
  2. @Contended:JVM自动填充字节使其独占缓存行,需通过-XX:-RestrictContended启用(如LongAdder的Cell)。