0
点赞
收藏
分享

微信扫一扫

Java 内存模型详解

栖桐 05-03 06:00 阅读 28

Java 内存模型(Java Memory Model,简称 JMM),是 Java 并发编程的核心底层规范,主要用于描述 Java 中内存对象的可见性处理逻辑。它定义了线程和主内存之间的交互规则,解决多线程环境下数据不一致、指令执行顺序混乱等问题,是理解 volatile、synchronized 等关键字底层原理的基石。


多线程交互的本质:共享内存的「数据对话」

多线程之间存在两种交互方式:

  1. 通过内存共享实现交互:线程间通过读写共享变量传递信息(Java 采用此方式)
  2. 通过交互实现内存共享:线程间通过消息传递机制间接同步数据(如 Erlang 并发模型)

Java 选择内存共享模式,意味着所有线程操作的变量均存储在主内存中,线程自身持有变量的副本(位于 CPU 缓存或寄存器)。JMM 提供了 volatilesynchronizedfinal 三个关键关键字,分别针对并发编程的三大核心问题:

  • 可见性:一个线程修改变量后,其他线程能否立即看到变化(volatile/synchronized/final
  • 原子性:操作是否具备「不可分割」的特性(synchronized/CAS)
  • 有序性:指令执行顺序是否与代码编写顺序一致(volatile/synchronized

而 CPU 缓存机制(数据副本不一致)与指令重排序(编译器 / 处理器优化导致顺序改变),是破坏数据可见性的两大核心技术挑战。


重排序:编译器与处理器的「性能优化双刃剑」

无论是编译器还是处理器,都会对代码进行「性能优化」,其中「指令重排序」是重要手段 —— 在不改变程序最终结果的前提下,调整指令执行顺序以提高效率。


1. 编译器重排序

  • 发生阶段:编译期(javac 生成字节码时)
  • 优化手段:删除冗余指令、合并计算、调整循环内不变量(如将循环内的常量计算移到循环外)
  • 典型案例

int a = 1;  
int b = 2;  
int c = a + b;  // 编译器可能重排为 b=2 → a=1 → c=3(结果不变)


2. 处理器重排序

  • 发生阶段:运行期(CPU 执行指令时)
  • 优化手段:利用流水线并行执行指令、缓冲写操作(Store Buffer)异步刷入主存
  • 典型影响
    若线程 A 先写变量 x 再写 y,CPU 可能因缓冲写优化,先写 y 再写 x,导致线程 B 读取时看到「y 已更新但 x 未更新」的中间状态。


伪共享:CPU 缓存行的「连带失效陷阱」

CPU 缓存以缓存行(Cache Line,通常 64 字节)为单位存储数据,缓存一致性协议(如 MESI)保证缓存与主存的数据同步,但一次失效操作会针对整个缓存行。


问题场景

假设变量 A(8 字节)与 B(8 字节)存储在同一缓存行:

  • 线程 1 修改 A,触发缓存行失效,线程 2 访问 B 时需从主存重新加载(即使 B 未被修改)
  • B 的缓存因「非自身原因」频繁失效,导致性能下降,即「伪共享」(False Sharing)。


解决方案

  • 空间换时间:让变量独占缓存行,避免其他变量与其共存。
  • Java 实现
    Java 1.8 引入 @sun.misc.Contended 注解,在变量或类级别添加填充字节:

@sun.misc.Contended // 需加 JVM 参数 -XX:-RestrictContended 禁用限制
class Counter {  
    private volatile long value = 0;  // 自动填充至 64 字节,独占缓存行
}

  • 该注解会在目标变量前后添加填充数据,确保其单独占据一个缓存行,消除伪共享影响。


汇编指令 lock:CPU 硬件级的同步基石

lock 是 Intel CPU 提供的硬件级指令,用于解决多 CPU 环境下的缓存一致性问题,是 volatile 和 CAS 的底层实现基础。


核心功能

  1. 锁定范围进化
  • Pentium 及之前:锁定系统总线,阻止其他 CPU 访问内存(性能开销大)
  • 新架构(如 Core):锁定目标缓存行(Cache Line Locking),通过 MESI 协议广播变更,仅影响当前缓存行(高效)
  1. 指令屏障作用:禁止 CPU 对 lock 指令前后的操作进行重排序(保障有序性)
  2. 强制数据同步:将当前 CPU 缓存的数据刷入主存,并使其他 CPU 对应缓存行失效(保障可见性)


典型应用

lock 指令是 volatile 写操作的底层实现 —— 当线程写入 volatile 变量时,JIT 会生成 lock 前缀的汇编指令,确保数据立即同步到主存并通知其他线程。


汇编指令 cmpxchg:AQS 底层的「原子比较魔法」

cmpxchg(Compare and Exchange)是 CPU 提供的原子比较交换指令,Java 中的 Unsafe.compareAndSwapXXX 方法(如 compareAndSwapInt)依赖该指令实现。


指令逻辑

cmpxchg target, expected_value, new_value  
; 若 target == expected_value,则设置 target = new_value,否则不改变

多 CPU 环境下,cmpxchg 会添加 lock 前缀,确保操作原子性:

lock cmpxchg [address], eax  ; 锁定缓存行,保证比较-交换操作不可分割


在 AQS 中的作用

Java 并发包的核心框架 AQS(AbstractQueuedSynchronizer),通过 cmpxchg 实现自旋锁:

  • 获取锁时,用 CAS 尝试修改状态(如 state 变量)
  • 释放锁时,通过 CAS 唤醒等待线程
    该机制确保 AQS 在获取 / 释放锁时具备内存可见性、原子性和有序性,是 ReentrantLockSemaphore 等工具的底层支撑。


原子性操作 CAS:无锁编程的「高效与风险」

CAS(Compare-And-Swap,比较并交换)是实现无锁并发的核心机制,通过 CPU 自旋实现原子操作,无需操作系统介入。


核心流程

  1. 读取当前值(V):获取共享变量的当前值
  2. 比较期望值(A):判断当前值是否等于预期值
  3. 交换新值(B):若相等则更新为新值,否则重试(自旋)


基于缓存锁定的原子性

CAS 通过「缓存锁定」(Cache Locking)保证原子性:

  • 写入数据后,通过 MESI 协议使其他 CPU 缓存的该变量失效
  • 后续读取时强制从主存加载最新值,确保数据一致性


三大核心问题

  1. ABA 问题
  • 场景:变量从 A→B→A,CAS 误认为未修改(如链表节点删除后重建)
  • 解决:引入版本号,使用 AtomicStampedReference(记录值 + 时间戳)
  1. 单一变量限制
  • 局限:仅支持单个变量原子操作,多变量需封装为对象
  • 解决:用 AtomicReference 包裹对象,保证对象引用的原子性
  1. 自旋开销
  • 风险:竞争激烈时,线程长时间自旋导致 CPU 占用率飙升
  • 优化:结合「自适应自旋」(JVM 动态调整自旋次数)或切换为锁机制


对象头:JVM 实现锁升级的「状态寄存器」

Java 对象头(Object Header)是 JVM 管理对象的核心数据结构,64 位系统中占 16 字节(8 字节 MarkWord + 8 字节 Class Pointer),数组对象额外包含 4 字节数组长度。


核心组成

  1. MarkWord(8 字节)
  • 无锁状态:存储 HashCode(25 位)、分代年龄(4 位)、锁状态(1 位偏向锁标志 + 2 位锁状态)
  • 偏向锁:存储当前持有锁的线程 ID(54 位)、分代年龄(4 位)、锁状态(2 位)
  • 轻量级锁:存储指向线程栈中锁记录的指针(62 位)、锁状态(2 位)
  • 重量级锁:存储指向操作系统互斥锁的指针(62 位)、锁状态(2 位)
  1. Class Pointer:指向对象的 Class 元数据,用于判断对象类型
  2. Array Length(数组专有):记录数组长度,非数组对象无此字段


锁状态变化

  • 偏向锁(无竞争):首次加锁时记录线程 ID,后续直接复用(零开销)
  • 轻量级锁(轻度竞争):通过 CAS 自旋尝试获取锁,避免线程阻塞
  • 重量级锁(激烈竞争):自旋超时后升级为 OS 级锁,线程进入阻塞队列


volatile:轻量级可见性与有序性保障

volatile 是 Java 中轻量级的并发关键字,专门解决多线程环境下的变量可见性和指令重排序问题。


两大核心保障

  1. 编译期:插入内存屏障(Memory Barrier)
  • 写屏障:确保 volatile 写操作前的所有指令已执行完毕,且结果对后续操作可见
  • 读屏障:确保 volatile 读操作后的所有指令在读取之后执行
  • 禁止重排序:编译器无法调整 volatile 读写操作与其他指令的相对顺序
  1. 运行期:CPU 级数据同步
  • 在 x86 架构下,JIT 会为 volatile 写操作生成 lock addl $0, (%esp) 指令(无实际计算,仅触发锁机制)
  • 锁缓存行:锁定目标缓存行,刷回主存并广播失效事件
  • 强制读取主存:其他线程检测到缓存失效后,必须从主存重新加载数据


典型使用场景

  • 状态标记变量:如 volatile boolean running = true;(线程安全的停止标志)
  • 单例模式 DCL 优化

public class Singleton {  
    private static volatile Singleton instance;  // 禁止指令重排序,避免返回未初始化对象  
    // ...  
}

  • 注意:volatile 不保证原子性(如 i++ 需配合 AtomicInteger


synchronized:从「偏向」到「重量级」的全链路锁升级

synchronized 是 JVM 内置的同步机制,通过「锁升级」策略在不同竞争场景下动态调整锁状态,兼顾性能与正确性。


1. 偏向锁(无竞争场景)

  • 核心逻辑:首次进入同步块时,在 MarkWord 中记录当前线程 ID,后续访问直接对比线程 ID(无需真实加锁)
  • 升级触发:当其他线程尝试获取锁时,通过 CAS 竞争失败,偏向锁升级为轻量级锁
  • 可见性保障:依赖 synchronized 块退出时的「隐式内存屏障」,强制刷新主存数据


2. 轻量级锁(轻度竞争)

  • 核心逻辑
  1. 线程在栈中创建「锁记录」,存储对象头 MarkWord 的副本
  2. 通过 CAS 将 MarkWord 替换为指向锁记录的指针(获取锁)
  3. 竞争失败则自旋重试(默认自旋 10 次,JVM 可动态调整)
  • 优势:避免线程挂起的上下文切换开销,适合短时间竞争场景


3. 重量级锁(激烈竞争)

  • 核心逻辑:自旋超时后,通过 park() 方法将线程挂起,放入操作系统的等待队列
  • 释放逻辑:解锁时调用 unpark() 唤醒线程,触发上下文切换(开销较大)
  • 适用场景:锁竞争激烈、持有锁时间较长的场景


锁优化最佳实践

  • 缩小同步范围:仅在必要代码块加锁(如用 synchronized(this) 替代 synchronized(class)
  • 结合显式锁:使用 ReentrantLock 的 tryLock() 避免永久阻塞


final:构造函数内的「重排序封印」

final 关键字不仅表示「变量不可变」,更在 JMM 层面提供了底层保障,确保其他线程不会读取到未初始化的 final 变量。


两大底层机制

  1. 编译器约束
  • 禁止将 final 变量的初始化操作移到构造函数之外
  • 非 final 变量可能在构造函数外初始化(如默认值初始化)
  1. 处理器约束
  • 构造函数结束前,禁止将对象引用(this)暴露给其他线程
  • 通过内存屏障,确保 final 变量赋值操作在构造函数内完成


反模式警示

public class FinalProblem {  
    final int x;  
    static FinalProblem instance;  

    public FinalProblem() {  
        x = 10;  
        instance = this;  // 危险!若此时被其他线程读取,x 可能未初始化(JMM 禁止此行为)  
    }  
}

JMM 保证:只要通过正常构造函数初始化,其他线程看到的 final 变量必定是已赋值的状态,避免「半初始化对象」问题。


总结:JMM 核心技术图谱

JMM 三大核心问题:
可见性 → volatile(内存屏障 + lock 指令)、synchronized(锁释放 / 获取的内存语义)、final(构造函数内初始化)
原子性 → synchronized(互斥锁)、CAS(cmpxchg 指令 + 自旋)
有序性 → volatile(禁止重排序)、synchronized(管程进入 / 退出的有序性)

底层硬件机制:
CPU 缓存 → 伪共享(@Contended 注解填充缓存行)
指令重排序 → 编译器重排序(优化代码)+ 处理器重排序(流水线优化)
汇编指令 → lock(缓存行锁定,保障可见性 + 有序性)、cmpxchg(CAS 原子操作)

关键字对比:

关键字

可见性

原子性

有序性

锁机制

适用场景

volatile





状态标记、轻量同步

synchronized




锁升级

临界区保护

final





不可变变量

并发工具底层:
AQS → CAS(cmpxchg)+ volatile(state 变量)+ 双向链表(等待队列)
原子类 → Unsafe.compareAndSwapXXX(底层 cmpxchg 指令)

举报

相关推荐

0 条评论