作者:黑马@梦想 | 来源:互联网 | 2024-12-26 01:14
本文探讨了并发编程中的关键设计原则,特别是Java内存模型(JMM)的happens-before规则及其对多线程编程的影响。文章详细介绍了DCL双重检查锁定模式的问题及解决方案,并总结了不同处理器和内存模型之间的关系,旨在为程序员提供更深入的理解和最佳实践。
并发编程是现代软件开发中不可或缺的一部分,特别是在多核处理器日益普及的今天。为了确保程序在多线程环境下的正确性和性能,理解其设计原理至关重要。本文将深入探讨Java内存模型(JMM)的设计要求、happens-before规则以及双重检查锁定(DCL)模式的相关问题及其解决方案。
1. happens-before 规则
1.1 JMM 设计要求
JMM的设计需兼顾程序员的易用性和编译器、处理器的高效性。具体来说:
JMM的基本原则是:只要不改变单线程或正确同步的多线程程序的执行结果,编译器和处理器可以自由优化。对于会改变执行结果的重排序,JMM要求禁止;对于不会改变结果的重排序,则不做限制。
1.2 happens-before 定义
happens-before规则定义如下:
happens-before关系和as-if-serial语义类似,前者保证多线程程序的正确性,后者保证单线程程序的顺序一致性。两者都旨在通过适当的重排序来提升程序的并行度。
1.3 happens-before 规则
序号 |
规则 |
内容 |
---|
1 |
程序顺序规则 |
一个线程内的操作先于该线程的后续操作 |
2 |
锁规则 |
解锁操作先于后续加锁操作 |
3 |
volatile变量规则 |
volatile变量写操作先于后续读操作 |
4 |
传递规则 |
A 先于 B,B 先于 C,则 A 先于 C |
5 |
线程启动规则 |
Thread对象的start()方法先于线程操作 |
6 |
线程中断规则 |
线程中断操作先于获取中断信息 |
7 |
线程终结规则 |
线程的所有操作先于线程死亡 |
8 |
对象终结规则 |
对象初始化完成先于finalize()方法 |
9 |
join规则 |
B线程中任意操作先于B线程返回 |
2. DCL 双重检查锁定
2.1 延迟初始化
延迟初始化仅在需要时才创建对象,以节省资源。例如:
public class DoubleCheckedLocking {
private static volatile Instance instance;
public static Instance getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckedLocking.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Instance(); // 延迟初始化
}
}
}
return instance;
}
}
然而,这种实现可能会出现问题,因为构造对象的操作可能会被重排序。
2.2 问题原因
初始化代码 instance = new Instance();
可分解为三个步骤:
memory = allocate(); // 分配内存空间
ctorInstance(memory); // 初始化内存空间
instance = memory; // 将instance指向内存空间
某些编译器可能重排序步骤2和步骤3,导致其他线程在对象未完全初始化时访问到它。
3. DCL 问题解决方案
3.1 基于 volatile 解决
将 instance
声明为 volatile
类型,可以禁止重排序,确保对象在完全初始化后才可见。例如:
private static volatile Instance instance;
这种方式适用于静态字段和实例字段的延迟初始化。
3.2 基于类初始化解决
另一种线程安全的延迟初始化方案是利用类初始化机制:
public class InstanceFactory {
private static class InstanceHolder {
public static final Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance;
}
}
JVM会在首次访问 getInstance()
方法时自动初始化 InstanceHolder
类,确保线程安全。
3.3 类初始化处理流程
每个类都有一个唯一的初始化锁,确保类只被初始化一次。类初始化分为五个阶段:
- 获取锁:线程A获取锁,设置状态为初始化中,释放锁;线程B等待。
- 执行初始化:线程A执行静态初始化,线程B等待。
- 初始化完毕:线程A重新获取锁,设置状态为已初始化,唤醒所有等待线程,释放锁。
- 结束类初始化:其他线程获取锁,确认已初始化,释放锁。
- 其他线程初始化:后续线程直接确认已初始化,不再重复初始化。
4. 内存模型总结
4.1 处理器内存模型
不同处理器的内存模型强度不同,追求高性能的处理器允许更多的重排序。JMM屏蔽了这些差异,为Java程序员提供了一致的内存模型。
内存模型 |
写-读重排序 |
写-写重排序 |
读-读/读-写重排序 |
可更早读取其他处理器的写 |
可更早读取当前处理器的写 |
内存模型强度 |
---|
TSO |
Y |
N |
N |
N |
Y |
4(最强) |
PSO |
Y |
Y |
N |
N |
Y |
3 |
RMO |
Y |
Y |
Y |
N |
Y |
2 |
PowerPC |
Y |
Y |
Y |
Y |
Y |
1(最弱) |
4.2 各种内存模型的关系
JMM是语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性模型是理论参考模型。内存模型强度:顺序一致性模型 > JMM > 处理器(TSO~PPC)。
4.3 JMM 内存可见性保证
4.4 JSR-133 语义增强