马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
Java 内存模型
Java 内存模型(Java Memory Model,简称 JMM),是 Java 并发编程的焦点底层规范,重要用于描述 Java 中内存对象的可见性处理逻辑。它界说了线程和主内存之间的交互规则,解决多线程环境下数据不同等、指令实行顺序杂乱等问题,是理解 volatile、synchronized 等关键字底层原理的基石。
多线程交互的本质:共享内存的「数据对话」
多线程之间存在两种交互方式:
- 通过内存共享实现交互:线程间通过读写共享变量传递信息(Java 采用此方式)
- 通过交互实现内存共享:线程间通过消息传递机制间接同步数据(如 Erlang 并发模型)
Java 选择内存共享模式,意味着所有线程操作的变量均存储在主内存中,线程自身持有变量的副本(位于 CPU 缓存或寄存器)。JMM 提供了 volatile、synchronized、final 三个关键关键字,分别针对并发编程的三大焦点问题:
- 可见性:一个线程修改变量后,其他线程可否立即看到变化(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 的底层实现底子。
焦点功能
- 锁定范围进化:
- Pentium 及之前:锁定体系总线,阻止其他 CPU 访问内存(性能开销大)
- 新架构(如 Core):锁定目标缓存行(Cache Line Locking),通过 MESI 协议广播变更,仅影响当前缓存行(高效)
- 指令屏障作用:禁止 CPU 对 lock 指令前后的操作举行重排序(保障有序性)
- 逼迫数据同步:将当前 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 在获取 / 释放锁时具备内存可见性、原子性和有序性,是 ReentrantLock、Semaphore 等工具的底层支撑。
原子性操作 CAS:无锁编程的「高效与风险」
CAS(Compare-And-Swap,比较并交换)是实现无锁并发的焦点机制,通过 CPU 自旋实现原子操作,无需操作体系介入。
焦点流程
- 读取当前值(V):获取共享变量的当前值
- 比较期望值(A):判断当前值是否等于预期值
- 交换新值(B):若相等则更新为新值,否则重试(自旋)
基于缓存锁定的原子性
CAS 通过「缓存锁定」(Cache Locking)保证原子性:
- 写入数据后,通过 MESI 协议使其他 CPU 缓存的该变量失效
- 后续读取时逼迫从主存加载最新值,确保数据同等性
三大焦点问题
- ABA 问题:
- 场景:变量从 A→B→A,CAS 误认为未修改(如链表节点删除后重建)
- 解决:引入版本号,使用 AtomicStampedReference(记录值 + 时间戳)
- 单一变量限定:
- 局限:仅支持单个变量原子操作,多变量需封装为对象
- 解决:用 AtomicReference 包裹对象,保证对象引用的原子性
- 自旋开销:
- 风险:竞争激烈时,线程长时间自旋导致 CPU 占用率飙升
- 优化:联合「自适应自旋」(JVM 动态调整自旋次数)或切换为锁机制
对象头:JVM 实现锁升级的「状态寄存器」
Java 对象头(Object Header)是 JVM 管理对象的焦点数据结构,64 位体系中占 16 字节(8 字节 MarkWord + 8 字节 Class Pointer),数组对象额外包含 4 字节数组长度。
焦点组成
- MarkWord(8 字节):
- 无锁状态:存储 HashCode(25 位)、分代年龄(4 位)、锁状态(1 位偏向锁标记 + 2 位锁状态)
- 偏向锁:存储当前持有锁的线程 ID(54 位)、分代年龄(4 位)、锁状态(2 位)
- 轻量级锁:存储指向线程栈中锁记录的指针(62 位)、锁状态(2 位)
- 重量级锁:存储指向操作体系互斥锁的指针(62 位)、锁状态(2 位)
- Class Pointer:指向对象的 Class 元数据,用于判断对象类型
- Array Length(数组专有):记录数组长度,非数组对象无此字段
锁状态变化
- 偏向锁(无竞争):首次加锁时记录线程 ID,后续直接复用(零开销)
- 轻量级锁(轻度竞争):通过 CAS 自旋尝试获取锁,避免线程阻塞
- 重量级锁(激烈竞争):自旋超时后升级为 OS 级锁,线程进入阻塞队列
volatile:轻量级可见性与有序性保障
volatile 是 Java 中轻量级的并发关键字,专门解决多线程环境下的变量可见性和指令重排序问题。
两大焦点保障
- 编译期:插入内存屏障(Memory Barrier)
- 写屏障:确保 volatile 写操作前的所有指令已实行完毕,且效果对后续操作可见
- 读屏障:确保 volatile 读操作后的所有指令在读取之后实行
- 禁止重排序:编译器无法调整 volatile 读写操作与其他指令的相对顺序
- 运行期: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. 轻量级锁(轻度竞争)
- 焦点逻辑:
- 线程在栈中创建「锁记录」,存储对象头 MarkWord 的副本
- 通过 CAS 将 MarkWord 替换为指向锁记录的指针(获取锁)
- 竞争失败则自旋重试(默认自旋 10 次,JVM 可动态调整)
- 优势:避免线程挂起的上下文切换开销,得当短时间竞争场景
3. 重量级锁(激烈竞争)
- 焦点逻辑:自旋超时后,通过 park() 方法将线程挂起,放入操作体系的等待队列
- 释放逻辑:解锁时调用 unpark() 叫醒线程,触发上下文切换(开销较大)
- 实用场景:锁竞争激烈、持有锁时间较长的场景
锁优化最佳实践
- 缩小同步范围:仅在须要代码块加锁(如用 synchronized(this) 替代 synchronized(class))
- 联合显式锁:使用 ReentrantLock 的 tryLock() 避免永久阻塞
final:构造函数内的「重排序封印」
final 关键字不仅表现「变量不可变」,更在 JMM 层面提供了底层保障,确保其他线程不会读取到未初始化的 final 变量。
两大底层机制
- 编译器束缚:
- 禁止将 final 变量的初始化操作移到构造函数之外
- 非 final 变量大概在构造函数外初始化(如默认值初始化)
- 处理器束缚:
- 构造函数结束前,禁止将对象引用(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 指令)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |