1. happens-before
1.1 JMM的设计要求
设计JMM时需要考虑:
-
程序员对内存模型的使用。希望内存模型易于理解。
JMM的happens-before规则简单易懂,向程序员提供了足够强的内存可见性保证 -
编译器和处理器对内存模型的实现。希望内存模型对编译器和处理器限制越少越好(便于优化)。
JMM的基本原则:只要不该变程序(单线程和正确同步)的执行结果,编译器和处理器怎么优化都可以
JMM的重排序策略:
- 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序
- 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求,可以进行重排序
1.2 happens-before的定义
happens-before的定义如下:
- 如果 A happens-before B ,那么 A 的执行结果将对 B 可见,而且 A 的执行顺序排在 B 之前
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果与按happens-before关系执行的结果一致,这种重排序并不非法
happens-before关系和as-if-serial语义是一回事:
-
as-if-serial语义保证单线程内程序执行结果不改变,happens-before关系保证正确同步的多线程程序执行结果不被改变
-
as-if-serial语义可以让程序员认为:单线程是按程序的顺序来执行的
happens-before关系可以让程序员认为:正确同步的多线程是按先行性规则指定的顺序来执行的
-
as-if-serial和happens-before都是为了在不改变程序执行结果的情况下,尽可能提高程序的并行度
1.3 happens-before规则(先行性规则)
序号 | 规则 | 内容 |
---|---|---|
1 | 程序顺序规则 | 一个线程内的任意操作,先于该线程中任意后续操作 |
2 | 锁规则 | 对一个锁的解锁先于之后对这个锁的加锁 |
3 | volatile变量规则 | 对一个volatile变量的写, 先于之后对该变量的读 |
4 | 传递规则 | A先于B,B先于C,则A先于C |
5 | 线程启动原则 | Thread对象的start()方法先于对该线程的任何操作 |
6 | 线程中断原则 | 线程执行interrupt操作先于获取到中断信息 |
7 | 线程终结规则 | 线程的所有操作先于线程死亡 |
8 | 对象终结规则 | 一个对象的初始化完成先于finalize()方法 |
9 | join规则 | ThreadB.join(),B线程中任意操作先于B线程返回 |
2. DCL双重检查锁定
2.1 延迟初始化
只有使用该对象(高开销)才进行初始化操作:
public class DoubleCheckedLocking{
private static Instance instance;
public static Instance getInstance(){
if(instance == null){ //第一次检查
synchroinzed (DoubleCheckedLocking.class){ //加锁
if(instance == null){ //第二次检查
instance=new Instance(); //延迟初始化(有问题)
}
}
}
}
}
大多数情况下正常初始化要优于延迟初始化。
2.2 出现问题的原因
初始化代码instance=new Instance();
可被分解为下面三个操作:
memory = allocate(); //1.分配内存空间
ctorInstance(memory); //2.初始化内存空间
instance = memory; //3.将instance指向内存空间
操作2,3在某些编译器中会重排序,其他线程在操作3之后,操作2之前获取锁访问instance对象就会出错(未初始化)
3. DCL问题解决方案
两种解决方案:
- 禁止重排(3.1)
- 允许重排但是对其他线程不可见(3.2)
3.1 基于volatile解决
将instance声明为volatile型,修改为private volatile static Instance instance;
就可以。
会禁止操作2,3之间的重排序(volatile写和写之前的操作)
基于volatile的方案处理可以对静态字段实现延迟初始化,还可以对实例字段实现延迟初始化。
3.2 基于类初始化解决
JVM在类的初始化阶段会执行类的初始化,在此期间,JVM会获取一个锁,同步多个线程对同一个类的初始化。
另一种线程安全的延迟初始化方案(不使用DCL),代码如下:
public class InstanceFactory{
private static class InstanceHolder{
public static Instance instance=new Instance();
}
public static Instance getInstance(){
//初始化InstanceHolder类
return InstanceHolder.instance;
}
}
两个线程并发访问getInstance示意图:(初始化锁)
立即初始化的五种情况:
- T是一个类,一个T类型的实例被创建
- T是一个类,且T中声明的一个静态方法被调用
- T中声明的一个静态字段被赋值
- T中声明的一个静态字段被使用,而且这个字段不是一个常量字段
- T是一个顶级类且一个断言语句嵌套在T内部执行
3.3 类初始化处理流程
Java的每一个类和接口C,都有一个唯一的初始化锁LC与之对应,每个线程都会至少获取一次该锁确保这个类已经被初始化。类的初始化流程分为五个阶段:
1. 获取锁
线程A:获取到初始化锁
-
读取到state=initialization,将其设置为initializing
-
释放初始化锁
线程B:等待获取初始化锁
2. 执行初始化
线程B:获取到初始化锁,读取到state=initializing,释放初始化锁并进入对应的condition等待
线程A:执行类的静态初始化和初始化类中声明的静态字段
3. 初始化完毕
线程A:获取初始化锁,设置state=initialized,唤醒初始化锁对应的condition中等待的所有线程
- 释放初始化锁,A线程初始化结束
线程B:被唤醒
4. 结束类初始化
线程B:获取初始化锁,读取到state=initialized,释放初始化锁,B线程初始化结束
5. 其他线程初始化
线程C:获取初始化锁,读取到state=initialized,释放初始化锁,C线程初始化结束
AB线程分别获取两次初始化锁,初始化完毕后的C线程只获取一次初始化锁
4. 内存模型总结
4.1 处理器内存模型
内存模型 | 写-读重排序 | 写-写重排序 | 读读和读写重排序 | 可以更早读取到其他处理器的写 | 可以更早读取到当前处理器的写 | 内存模型强度 |
---|---|---|---|---|---|---|
TSO | Y | Y | 4(最强) | |||
PSO | Y | Y | Y | 3 | ||
RMO | Y | Y | Y | Y | 2 | |
PowerPC | Y | Y | Y | Y | Y | 1(最弱) |
越是追求性能的处理器模型越弱,允许越多的重排序,对处理器的束缚越少。
JMM屏蔽了不同的处理器内存模型的差异,在不同的模拟器上为Java程序员提供了一个一致的内存模型。
4.2 各种内存模型的关系
JMM是语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性模型是理论参考模型。
内存模型强度:顺序一致性模型>JMM>处理器(TSO~PPC)
4.3 JMM内存可见性保证
-
单线程程序:不会出现内存可见性问题
-
正确同步的多线程程序:具有顺序一致性,JMM通过限制编译器和处理器重排序来为程序员提供内存可见性保证
-
未同步/未正确同步的多线程程序:
最小安全性保证:线程执行时读到的值,要么是之前线程写入的值(64位long或double可能只写入一半),要么是默认值
4.4 JSR-133语义增强
-
volatile内存语义增强:严格限制volatile变量和普通变量之间的重排序
-
final内存语义增强:保证final引用不会从构造函数内溢出