Java内存模型(Java Memory Model, JMM)是Java并发编程的核心基础,它定义了多线程环境下变量的访问规则,决定了在什么时候一个线程对共享变量的修改对其他线程可见。本文将全面剖析JMM的各个方面,从基础概念到高级应用,帮助开发者彻底理解并正确运用这一关键机制。
一、JMM概述:为什么需要内存模型?
1.1 计算机体系结构的内存问题
现代计算机体系结构中存在多个层次的内存访问优化,这导致了内存访问的一些特殊现象:
-
CPU缓存架构:多级缓存(L1/L2/L3)的存在导致内存可见性问题
-
指令重排序:编译器/处理器为了优化性能可能改变指令执行顺序
-
多核并发:不同CPU核心可能看到不同的内存状态
java
// 典型的内存可见性问题示例
public class VisibilityProblem {
private static boolean ready = false;
private static int number = 0;
public static void main(String[] args) {
new Thread(() -> {
while (!ready) {
// 可能永远循环!
}
System.out.println(number);
}).start();
number = 42;
ready = true;
}
}
1.2 JMM的作用与意义
Java内存模型的主要目标:
-
定义规则:规定线程如何与内存交互
-
保证可见性:确保一个线程的修改对其他线程可见
-
禁止重排序:限制编译器和处理器的优化行为
-
提供同步机制:定义happens-before关系
二、JMM核心概念详解
2.1 主内存与工作内存
JMM将内存抽象为两种:
-
主内存(Main Memory):所有共享变量的存储位置
-
工作内存(Working Memory):每个线程私有的内存空间
变量访问流程:
-
线程从主内存拷贝变量到工作内存
-
线程在工作内存中操作变量
-
在某个时刻将工作内存的值刷新回主内存
2.2 内存间交互操作
JMM定义了8种原子操作来完成主内存与工作内存的交互:
操作 | 作用 |
---|---|
lock(锁定) | 作用于主内存,标识变量为线程独占 |
unlock(解锁) | 释放锁定状态 |
read(读取) | 从主内存传输变量到工作内存 |
load(载入) | 把read得到的值放入工作内存副本 |
use(使用) | 把工作内存值传递给执行引擎 |
assign(赋值) | 将执行引擎接收的值赋给工作内存变量 |
store(存储) | 把工作内存值传送到主内存 |
write(写入) | 把store得到的值放入主内存变量 |
操作规则:
-
read/load和store/write必须成对出现
-
不允许一个变量从主内存读取但工作内存不接受,或工作内存发起回写但主内存不接受
-
新变量只能在主内存"诞生"
-
一个变量同一时刻只允许一个线程lock
-
对变量执行lock操作会清空工作内存中此变量的值
-
unlock前必须先把变量同步回主内存
2.3 happens-before原则
happens-before是JMM的核心概念,定义了两个操作的偏序关系:
基本规则:
-
程序顺序规则:同一线程中的每个操作happens-before于该线程中的任意后续操作
-
监视器锁规则:对一个锁的解锁happens-before于随后对这个锁的加锁
-
volatile变量规则:对一个volatile域的写happens-before于任意后续对这个volatile域的读
-
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
-
线程启动规则:Thread.start()的调用happens-before于被启动线程中的任何操作
-
线程终止规则:线程中的任何操作都happens-before于其他线程检测到该线程已经终止
-
中断规则:一个线程调用另一个线程的interrupt happens-before于被中断线程检测到中断
-
终结器规则:对象的构造函数执行happens-before于它的finalize方法
java
// happens-before示例
public class HappensBeforeExample {
private int x = 0;
private volatile boolean v = false;
public void writer() {
x = 42; // (1)
v = true; // (2) volatile写
}
public void reader() {
if (v) { // (3) volatile读
System.out.println(x); // (4) 保证输出42
}
}
}
三、volatile关键字深度解析
3.1 volatile的语义
volatile变量具有两种特性:
-
可见性:对volatile变量的写操作会立即刷新到主内存
-
禁止重排序:编译器/处理器不会对volatile操作重排序
内存屏障:
-
写volatile变量时:在写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障
-
读volatile变量时:在读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障
3.2 volatile与普通变量的区别
特性 | 普通变量 | volatile变量 |
---|---|---|
可见性 | 不保证 | 保证 |
原子性 | 32位以下基本类型具有原子性 | 任何读写操作都具有原子性 |
重排序 | 允许 | 限制 |
性能 | 高 | 较低(因为内存屏障) |
3.3 volatile的正确使用场景
-
状态标志:
javapublic class ShutdownRequest extends Thread { private volatile boolean shutdownRequested = false; public void shutdown() { shutdownRequested = true; } @Override public void run() { while (!shutdownRequested) { // 执行任务 } } }
-
一次性安全发布:
javapublic class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
-
独立观察:
java
public class UserManager {
private volatile String lastUser;
public void authenticate(String user, String password) {
boolean valid = // 验证逻辑
if (valid) {
lastUser = user;
}
}
}
四、synchronized与锁的内存语义
4.1 synchronized的三大特性
-
原子性:确保互斥执行临界区代码
-
可见性:解锁前必须将变量刷新到主内存
-
有序性:限制临界区内指令重排序
4.2 锁的内存语义
-
获取锁:清空工作内存,从主内存重新加载变量
-
释放锁:将工作内存中的变量刷新到主内存
java
public class SynchronizedExample {
private int value = 0;
public synchronized void increment() {
value++; // 原子性+可见性保证
}
public int getValue() {
synchronized(this) {
return value; // 保证读取最新值
}
}
}
4.3 锁与volatile的比较
特性 | synchronized | volatile |
---|---|---|
原子性 | 保证代码块/方法级别的原子性 | 保证单个读/写的原子性 |
可见性 | 保证 | 保证 |
阻塞 | 是 | 否 |
适用范围 | 代码块/方法 | 变量 |
编译器优化 | 限制 | 限制 |
性能 | 较高开销 | 较低开销 |
五、final域的内存语义
5.1 final域的重排序规则
-
写final域:禁止把final域的写重排序到构造函数之外
-
读final域:初次读包含final域的对象引用时,保证final域已被初始化
5.2 final的正确使用
java
public class FinalExample {
private final int x;
private int y;
private static FinalExample instance;
public FinalExample() {
x = 1; // final写
y = 2; // 普通写
}
public static void writer() {
instance = new FinalExample();
}
public static void reader() {
FinalExample object = instance;
int a = object.x; // 保证读到1
int b = object.y; // 可能读到0
}
}
5.3 final与线程安全
正确构造的不可变对象是线程安全的:
java
public class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// 只有getter方法
}
六、双重检查锁定问题与解决方案
6.1 错误的双重检查锁定
java
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckedLocking.class) {
if (instance == null) { // 第二次检查
instance = new Instance(); // 问题根源!
}
}
}
return instance;
}
}
问题根源 :instance = new Instance()
可能被重排序为:
-
分配内存空间
-
将引用指向内存空间(此时instance!=null)
-
初始化对象
6.2 正确的解决方案
-
使用volatile:
javaprivate volatile static Instance instance;
-
静态内部类:
javapublic class Singleton { private static class Holder { static final Instance INSTANCE = new Instance(); } public static Instance getInstance() { return Holder.INSTANCE; } }
-
枚举实现:
javapublic enum Singleton { INSTANCE; public void someMethod() { ... } }
七、JMM与并发工具类
7.1 Atomic类的内存语义
原子类基于CAS实现,具有volatile读写的内存语义:
java
public class AtomicExample {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // 包含内存屏障
}
public int get() {
return counter.get(); // 具有volatile读语义
}
}
7.2 ConcurrentHashMap的内存保证
ConcurrentHashMap通过分段锁和volatile变量保证内存可见性:
java
static final class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // 值用volatile修饰
volatile Node<K,V> next; // 下一个节点也用volatile修饰
}
7.3 CountDownLatch的内存语义
java
public class CountDownLatchDemo {
private final CountDownLatch latch = new CountDownLatch(1);
private int result;
public void compute() {
new Thread(() -> {
result = doCompute(); // (1)
latch.countDown(); // (2) 释放存储屏障
}).start();
}
public int getResult() throws InterruptedException {
latch.await(); // (3) 获取加载屏障
return result; // (4) 保证看到(1)的结果
}
}
八、JMM实战问题与解决方案
8.1 伪共享(False Sharing)问题
问题现象:
-
多个线程频繁修改同一缓存行的不同变量
-
导致缓存行无效,性能下降
解决方案:
-
填充(Padding):
javapublic class FalseSharing { public volatile long value1; public long p1, p2, p3, p4, p5, p6, p7; // 填充 public volatile long value2; }
-
使用@Contended注解(Java8+):
javapublic class FalseSharing { @jdk.internal.vm.annotation.Contended public volatile long value1; @jdk.internal.vm.annotation.Contended public volatile long value2; }
8.2 安全发布模式
-
静态初始化:
javapublic static Holder holder = new Holder(42);
-
volatile或AtomicReference:
javapublic volatile Holder holder;
-
final域:
javapublic class HolderWrapper { public final Holder holder; public HolderWrapper(Holder holder) { this.holder = holder; } }
九、JMM与Java新特性
9.1 Java 9+的VarHandle
提供更灵活的内存访问操作:
java
public class VarHandleExample {
private int x;
private static final VarHandle X;
static {
try {
X = MethodHandles.lookup()
.findVarHandle(VarHandleExample.class, "x", int.class);
} catch (Exception e) {
throw new Error(e);
}
}
public void increment() {
X.getAndAdd(this, 1); // 原子操作
}
}
十、JMM最佳实践
-
-
优先使用高层并发工具:
-
使用ConcurrentHashMap而不是同步的HashMap
-
使用AtomicLong而不是synchronized计数器
-
-
最小化同步范围:
-
同步块比同步方法更好
-
只在必要时使用同步
-
-
正确发布共享对象:
-
使用final字段
-
使用volatile或正确同步
-
-
避免过度同步:
-
考虑使用不可变对象
-
使用线程封闭技术(ThreadLocal)
-
-
理解happens-before关系:
-
不要依赖执行时序
-
确保正确的同步
-
-
性能考虑:
-
测量而不是猜测
-
注意伪共享问题
-
考虑无锁算法
-
-