目录
- JMM核心内容概览与重要程度评级
- 1. JMM是什么?为什么需要JMM?
- 1.1 JMM的定义与作用
- 1.2 为什么需要JMM:硬件层面的挑战
- 2. JMM的核心架构:主内存与工作内存
- 3. ⭐⭐⭐⭐⭐ volatile关键字深度解析
- 3.1 volatile的语义与保证
- 3.2 volatile的实现原理
- 3.3 volatile的使用场景与限制
- 4. ⭐⭐⭐⭐⭐ synchronized的内存语义
- 5. ⭐⭐⭐⭐⭐ happens-before规则
- 5.1 happens-before规则详解
- 5.2 happens-before的数学基础
- 6. ⭐⭐⭐⭐⭐ 原子性、可见性、有序性
- 6.1 原子性(Atomicity)
- 6.2 可见性(Visibility)
- 6.3 有序性(Ordering)
- 7. ⭐⭐⭐⭐ 安全发布模式
- 8. ⭐⭐⭐⭐ 双重检查锁定(DCL)问题与解决方案
- 8.1 错误的DCL实现
- 8.2 正确的DCL实现
- 9. ⭐⭐⭐ final字段的内存语义
- 10. JMM在开发中的实际应用
- 10.1 性能优化建议
- 10.2 常见陷阱与避免方法
- 总结
JMM核心内容概览与重要程度评级
在学习JMM前,我们先了解其核心内容体系及重要程度:
| 内容模块 | 重要程度 | 说明 |
|---|---|---|
| 1. JMM基础概念 | ⭐⭐⭐⭐ | 理解JMM的出发点和基本架构 |
| - 硬件基础与并发挑战 | ⭐⭐⭐⭐ | 了解JMM存在的必要性 |
| - 主内存与工作内存 | ⭐⭐⭐⭐ | JMM的核心抽象概念 |
| 2.js 内存间交互操作 | ⭐⭐⭐ | JMM的基础操作定义 |
| 3. volatile关键字 | ⭐⭐⭐⭐⭐ | 最常用的同步机制,必须深入掌握 |
| 4. synchronized内存语义 | ⭐⭐⭐⭐⭐ | 理解锁的内存效应 |
| 5. happens-before规则 | ⭐⭐⭐⭐⭐ | JMM的理论核心,解决可见性问题的关键 |
| 6. 原子性、可见性、有序性 | ⭐⭐⭐⭐⭐ | 并发编程的三大核心问题 |
| 7. 安全发布模式 | ⭐⭐⭐⭐ | 实际开发中的常用技巧 |
| 8. final字段语义 | ⭐⭐⭐ | 特殊但重要的内存语义 |
| 9. 双重检查锁定问题 | ⭐⭐⭐⭐ | 经典问题的分析与解决方案 |
| 10. JMM底层实现 | ⭐⭐⭐ | 理解原理,优化性能 |
接下来,我们将按照重要程度,逐一深入讲解各个模块。
1. JMM是什么?为什么需要JMM?
1.1 JMM的定义与作用
Java内存模型(Java Memory Model, JMM) 是Java虚拟机规范中定义的一种抽象规范,用于屏蔽各种硬件和操作系统的内存访问差异,实现Java程序在各种平台下都能达到一致的内存访问效果。
JMM的核心作用:
- 定义规则:规定多线程环境下变量的访问方式
- 提供保证:确保在不同平台上内存访问行为的一致性
- 允许优化:在保证正确性的前提下允许编译器和处理器进行优化
1.2 为什么需要JMM:硬件层面的挑战
现代计算机系统的多层次存储架构导致了并发编程的三大核心问题:
public class CophpncurrencyProblems {
private static boolean ready = false;
private static int number = 0;
public static void main(String[] args) {
// 线程1:数据准备
Thread writer = new Thread(() -> {
number = 42; // 操作1:可能被重排序到操作2之后
ready = true; // 操作2:可能先执行
});
// 线程2:数据处理
Thread reader = new Thread(() -> {
while (!ready) {
// 等待ready变为true
Thread.yield();
}
// 可能输出0而不是42!
System.out.println("Number: " + number);
});
writer.start();
reader.start();
}
}
问题根源:
- CPU缓存一致性:多核CPU各有缓存,数据更新不同步
- 指令重排序:编译器和处理器为优化性能重新排序指令
- 内存可见性:一个线程的修改对其他线程不可见
2. JMM的核心架构:主内存与工作内存
JMM通过抽象的内存模型解决上述问题:
工作内存与主内存的交互通过8种原子操作完成:
public class MemoryOperations {
private int sharedValue = 0;
public void operationExample() {
// 1. read: 从主内存读取变量到传输通道
// 2. load: 将read得到的值放入工作内存的变量副本
// 相当于: int temp = sharedValue; (但这是高级语言表示)
// 3. use: 将工作内存中的变量传递给执行引擎
int result = sharedValue * 2;
// 4. assign: 将执行引擎的结果赋给工作内存中的变量
sharedValue = result + 1;
// 5. store: 将工作内存中的变量值传输到主内存的传输通道
// 6. write: 将store获取的值放入主内存的变量
// 7. lock: 将主内存变量标记为线程独占状态
// 8. unlock: 释放锁定的变量
}
}
3. ⭐⭐⭐⭐⭐ volatile关键字深度解析
3.1 volatile的语义与保证
volatile是JVM提供的最轻量级的同步机制,提供两大保证:
- 可见性保证:对volatile变量的写操作立即对其他线程可见
- 禁止重排序:阻止编译器和处理器对volatile操作进行重排序
public class VolatileExample {
private volatile boolean flag = false;
private int value = 0;
public void writer() {
value = 42; // 普通写操作
// StoreStore内存屏障:禁止上面的普通写与下面的volatile写重排序
flag = true; // volatile写操作
// StoreLoad内存屏障:确保volatile写立即对其他处理器可见
}
public void reader() {
// LoadLoad内存屏障:确保volatile读之前的所有读操作已完成
if (flag) { // volatile读操作
// LoadStore内存屏障:确保volatile读之后的写操作不会重排序到读之前
System.out.println(value); // 保证看到value = 42
}
}
}
3.2 volatile的实现原理
在硬件层面,volatile通过内存屏障指令实现:
public class VolatileBarrier {
private volatile int value;
public void setValue(int newValue) {
this.value = newValue;
// 对应x86汇编代码:
// mov %eax,0x10(%rsi) ; 将newValue存入value的内存地址
// lock addl $0x0,(%rsp) ; StoreLoad内存屏障(mfence指令)
}
public int getValue() {
// volatile读在x86上不需要特殊指令
// 因为x86的内存模型已经保证了可见性(TSO模型)
return value;
}
}
内存屏障类型:
- LoadLoad屏障:禁止读操作重排序
- StoreStore屏障:禁止写操作重排序
- LoadStore屏障:禁止读与写操作重排序
- StoreLoad屏障:禁止写与读操作重排序(最重量级)
3.3 volatile的使用场景与限制
适用场景:
- 状态标志位
- 一次性安全发布
- 独立观察(independent observation)
- 开销较低的读-写锁策略
不适用场景:
- 复合操作(如i++)
- 依赖于当前值的操作(如value = value + 1)
public class VolatileUsage {
// 场景1:状态标志位
private volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// 执行工作任务
}
}
// 场景2:一次性安全发布
private volatile Resource resource;
public Resource getResource() {
if (resource == null) {
synchronized(this) {
if (resource == null) {
resource = new Resource(); // 安全发布
javascript }
}
}
return resource;
}
}
4. ⭐⭐⭐⭐⭐ synchronized的内存语义
synchronized不仅提供互斥执行,还提供重要的内存语义:
public class SynchronizedMemory {
private int counter = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
// monitorenter指令:
// 1. 清空工作内存
// 2. 从主内存重新加载所有共享变量
counter++;
// 临界区内的操作不会被重排序到临界区外
}
// monitorexit指令:
// 1. 将工作内存中的修改刷新到主内存
// 2. 释放锁
}
public int getCounter() {
synchronized(lock) {
// 获取锁会强制从主内存重新读取变量
return counter;
}
}
}
synchronized的内存语义:
- 进入同步块:清空工作内存,从主内存重新加载变量
- 退出同步块:将工作内存中的修改刷新到主内存
- 互斥执行:确保同一时刻只有一个线程执行临界区代码
5. ⭐⭐⭐⭐⭐ happens-before规则
happens-before是JMM的理论核心,定义了操作之间的可见性关系。
5.1 happens-before规则详解
public class HappensBeforeExample {
private int x = 0;
private volatile boolea编程客栈n v = false;
private int y = 0;
private final Object lock = new Object();
public void demo() {
// 规则1:程序次序规则
x = 1; // 操作A
v = true; // 操作B:A happens-before B
// 规则2:volatile变量规则
if (v) { // 操作C:B happens-before C
y = x; // 操作D:C happens-before D
}
// 规则3:传递性规则
// A happens-before B, B happens-before C, C happens-before D
// 因此 A happens-before D
// 规则4:管程锁定规则
synchronized(lock) { // 加锁E
x = 2; // 操作F:E happens-before F
} // 解锁G:F happens-before G
// 规则5:线程启动规则
Thread t = new Thread(() -> {
System.out.println(x); // 看到x=2
});
t.start(); // start() happens-before 线程中的所有操作
// 规则6:线程终止规则
try {
t.join(); // 线程中的所有操作 happens-before join()返回
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 规则7:对象终结规则
// 对象的构造函数执行结束 happens-before finalize()方法开始
}
}
5.2 happens-before的数学基础
happens-before关系是一个偏序关系,具有:
- 自反性:A happens-before A
- 反对称性:如果A happens-before B且B happens-before A,则A=B
- 传递性:如果A happens-before B且B happens-before C,则A happens-before C
6. ⭐⭐⭐⭐⭐ 原子性、可见性、有序性
这是并发编程的三大核心问题,JMM为每个问题提供了解决方案。
6.1 原子性(Atomicity)
原子性是指一个操作不可中断,要么全部执行成功,要么完全不执行。
public class AtomicityExample {
private int basicType = 0; // 基本类型访问是原子的
private long longValue = 0L; // long和double可能非原子(但现代JVM通常保证原子性)
private volatile boolean flag = false; // volatile保证单个读/写的原子性
// 复合操作不是原子的
public void nonAtomicIncrement() {
basicType++; // 不是原子操作!分解为read-modify-write三步
}
// 保证原子性的方式
private final AtomicInteger atomicInt = new AtomicInteger(0);
private final Object lock = new Object();
private int synchronizedValue = 0;
public void atomicOperations() {
// 方式1:使用原子类
atomicInt.incrementAndGet(); // 原子操作
// 方式2:使用同步
synchronized(lock) {
synchronizedValue++; // 原子操作
}
// 方式3:使用volatile变量(仅适用于特定场景)
flag = true; // 原子操作
}
}
6.2 可见性(Visibility)
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
public class VisibilityExample {
private int noVisibility = 0; // 无可见性保证
private volatile boolean hasVisibility = false; // volatile保证可见性
private int synchronizedValue = 0; // synchronized保证可见性
private final Object lock = new Object();
public void demonstrate() {
// 线程1:修改数据
new Thread(() -> {
noVisibility = 42;
hasVisibility = true;
synchronized(lock) {
synchronizedValue = 100;
}
}).start();
// 线程2:读取数据
new Thread(() -> {
// 可能看不到noVisibility的更新
while (!hasVisibility) {
// 等待hasVisibility变为true
}
// 保证看到noVisibility = 42(因为volatile写happens-before volatile读)
synchronized(lock) {
// 保证看到synchronizedValue = 100
}
}).start();
}
}
6.3 有序性(Ordering)
有序性是指程序执行的顺序按照代码的先后顺序执行。
public class OrderingExample {
private int x = 0;
private int y = 0;
private volatile boolean ready = false;
public void orderingDemo() {
// 线程1:可能被重排序
new Thread(() -> {
x = 1; // 操作1
y = 2; // 操作2
ready = true; // 操作3:volatile写,阻止重排序
}).start();
// 线程2
new Thread(() -> {
while (!ready) {
// 等待
}
// 由于volatile的语义,这里保证看到x=1, y=2
// 不会出现y=2但x=0的情况
}).start();
}
}
7. ⭐⭐⭐⭐ 安全发布模式
安全地发布对象是并发编程中的常见需求,JMM提供了多种模式。
public class SafePublication {
// 方式1:静态初始化器(最安全)
private static final Resource staticResource = new Resource();
// 方式2:volatile字段
private volatile Resource volatileResource;
public void initVolatileResource() {
volatileResource = new Resource(); // 安全发布
}
// 方式3:final字段
private final Resource finalResource;
public SafePublication() {
this.finalResource = new Resource(); // 安全发布
}
// 方式4:正常锁保护
private Resource guardedResource;
private final Object lock = new Object();
public void initGuardedResource() {
synchronized(lock) {
if (guardedResource == null) {
guardedResource = new Resource(); // 安全发布
}
}
}
// 方式5:线程安全容器
private final Map<String, Resource> safeMap
= Collections.synchronizedMap(new HashMap<>());
private final ConcurrentMap<String, Resource> concurrentMap
= new ConcurrentHashMap<>();
public void addToSafeMap(String key) {
safeMap.put(key, new Resource()); // 安全发布
}
}
8. ⭐⭐⭐⭐ 双重检查锁定(DCL)问题与解决方案
双重检查锁定是一个经典的并发模式,但存在陷阱。
8.1 错误的DCL实现
public class BrokenDCL {
private static Resource resource; // 没有volatile!
public static Resource getInstance() {
if (resource == null) { // 第一次检查(无锁)
synchronized(BrokenDCL.class) { // 加锁
if (resource == null) { // 第二次检查(有锁)
resource = new Resource(); // 问题所在!
// 可能发生的重排序:
// 1. 分配内存空间
// 2. 将引用指向内存空间(此时resource != null)
// 3. 初始化对象(还未执行)
// 其他线程可能拿到未完全初始化的对象!
}
}
}
return resource;
}
}
8.2 正确的DCL实现
public class CorrectDCL {
// 使用volatile禁止重排序
private static volatile CorrectDCL instance;
private final int value;
private final String name;
private CorrectDCL() {
this.value = 42; // 初始化final字段
this.name = "DCL"; // 初始化普通字段
// 构造函数执行
}
public static CorrectDCL getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized(CorrectDCL.class) { // 加锁
if (instance == null) { // 第二次检查(有锁)
instance = new CorrectDCL(); // 安全发布
// volatile写插入内存屏障,确保:
// 1. 所有初始化操作完成
// 2. 初始化结果对其他线程立即可见
}
}
}
return instance;
}
}
9. ⭐⭐⭐ final字段的内存语义
final字段在并发编程中有特殊的内存语义,提供了安全初始化的保证。
public class FinalFieldExample {
private final int finalValue; // final字段
private int normalValue; // 普通字段
private volatile boolean ready = false;
public FinalFieldExample() {
normalValue = 1; // 普通字段写入(可能被重排序)
finalValue = 42; // final字段写入
// JMM在此隐式插入StoreStore内存屏障
// 确保final字段的初始化不会被重排序到构造函数之外
ready = true; // volatile写
}
public static void reader() {
FinalFieldExample obj = new FinalFieldExajsmple();
// 保证看到finalValue的正确值(42)
// 即使没有同步,也能看到正确初始化的final字段
int r1 = obj.finalValue;
// 可能看到normalValue的默认值(0)而不是1
// 因为没有同步保证
int r2 = obj.normalValue;
// 但如果通过volatile读看到ready=true
// 那么也能保证看到所有字段的正确初始化值
if (obj.ready) {
// 保证看到finalValue=42和normalValue=1
}
}
}
10. JMM在开发中的实际应用
10.1 性能优化建议
- 减少同步范围:只在必要时使用同步
- 使用volatile代替锁:当只需要可见性保证时
- 使用线程局部变量:避免共享,消除同步
- 使用并发容器:代替手动同步的容器
10.2 常见陷阱与避免方法
public class CommonConcurrencyMistakes {
// 陷阱1:认为volatile保证原子性
private volatile int count = 0;
public void unsafeIncrement() {
count++; // 不是原子操作!
}
// 解决方案:使用原子类或同步
private final AtomicInteger safeCount = new AtomicInteger(0);
private int synchronizedCount = 0;
private final Object lock = new Object();
public void safeIncrement() {
safeCount.incrementAndGet(); // 方式1:原子类
synchronized(lock) { // 方式2:同步
synchronizedCount++;
}
}
// 陷阱2:误用双重检查锁定
// 解决方案:使用volatile修饰实例变量
// 陷阱3:依赖线程优先级
// 解决方案:不要依赖线程优先级进行正确性设计
// 陷阱4:在构造函数中启动线程
public class ProblematicConstructor {
public ProblematicConstructor() {
new Thread(() -> {
// 可能访问未完全初始化的对象
}).start();
}
}
}
总结
Java内存模型是Java并发编程的基石,它通过定义一系列规则和happens-before关系,在多线程环境中提供了内存可见性、原子性和有序性的保证。
关键要点:
- 理解happens-before规则:这是理解线程间操作可见性的核心
- 正确使用volatile:了解其适用场景和限制
- 掌握安全发布模式:确保对象在线程间安全共享
- 避免常见陷阱:识别并避免常见的并发编程错误
JMM既是一个规范也是一个工具,正确理解和使用JMM可以帮助我们编写出既正确又高效的多线程程序。在实际开发中,应该优先使用java.util.concurrent包提供的高级并发工具,它们在大多数情况下都能提供更好的性能和更简单的编程模型。
到此这篇关于Java内存模型(JMM)一文透彻理解的文章就介绍到这了,更多相关Java内存模型JMM内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
加载中,请稍侯......
精彩评论