设计JMM时需要考虑:
程序员对内存模型的使用。希望内存模型易于理解。
JMM的happens-before规则简单易懂,向程序员提供了足够强的内存可见性保证
编译器和处理器对内存模型的实现。希望内存模型对编译器和处理器限制越少越好(便于优化)。
JMM的基本原则:只要不该变程序(单线程和正确同步)的执行结果,编译器和处理器怎么优化都可以
JMM的重排序策略:
happens-before的定义如下:
happens-before关系和as-if-serial语义是一回事:
as-if-serial语义保证单线程内程序执行结果不改变,happens-before关系保证正确同步的多线程程序执行结果不被改变
as-if-serial语义可以让程序员认为:单线程是按程序的顺序来执行的
happens-before关系可以让程序员认为:正确同步的多线程是按先行性规则指定的顺序来执行的
as-if-serial和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线程返回 |
只有使用该对象(高开销)才进行初始化操作:
public class DoubleCheckedLocking{
private static Instance instance;
public static Instance getInstance(){
if(instance == null){ //第一次检查
synchroinzed (DoubleCheckedLocking.class){ //加锁
if(instance == null){ //第二次检查
instance=new Instance(); //延迟初始化(有问题)
}
}
}
}
}
大多数情况下正常初始化要优于延迟初始化。
初始化代码instance=new Instance();
可被分解为下面三个操作:
memory = allocate(); //1.分配内存空间
ctorInstance(memory); //2.初始化内存空间
instance = memory; //3.将instance指向内存空间
操作2,3在某些编译器中会重排序,其他线程在操作3之后,操作2之前获取锁访问instance对象就会出错(未初始化)
两种解决方案:
将instance声明为volatile型,修改为private volatile static Instance instance;
就可以。
会禁止操作2,3之间的重排序(volatile写和写之前的操作)
基于volatile的方案处理可以对静态字段实现延迟初始化,还可以对实例字段实现延迟初始化。
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示意图:(初始化锁)
立即初始化的五种情况:
Java的每一个类和接口C,都有一个唯一的初始化锁LC与之对应,每个线程都会至少获取一次该锁确保这个类已经被初始化。类的初始化流程分为五个阶段:
线程A:获取到初始化锁
读取到state=initialization,将其设置为initializing
释放初始化锁
线程B:等待获取初始化锁
线程B:获取到初始化锁,读取到state=initializing,释放初始化锁并进入对应的condition等待
线程A:执行类的静态初始化和初始化类中声明的静态字段
线程A:获取初始化锁,设置state=initialized,唤醒初始化锁对应的condition中等待的所有线程
线程B:被唤醒
线程B:获取初始化锁,读取到state=initialized,释放初始化锁,B线程初始化结束
线程C:获取初始化锁,读取到state=initialized,释放初始化锁,C线程初始化结束
AB线程分别获取两次初始化锁,初始化完毕后的C线程只获取一次初始化锁
内存模型 | 写-读重排序 | 写-写重排序 | 读读和读写重排序 | 可以更早读取到其他处理器的写 | 可以更早读取到当前处理器的写 | 内存模型强度 |
---|---|---|---|---|---|---|
TSO | Y | Y | 4(最强) | |||
PSO | Y | Y | Y | 3 | ||
RMO | Y | Y | Y | Y | 2 | |
PowerPC | Y | Y | Y | Y | Y | 1(最弱) |
越是追求性能的处理器模型越弱,允许越多的重排序,对处理器的束缚越少。
JMM屏蔽了不同的处理器内存模型的差异,在不同的模拟器上为Java程序员提供了一个一致的内存模型。
JMM是语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性模型是理论参考模型。
内存模型强度:顺序一致性模型>JMM>处理器(TSO~PPC)
单线程程序:不会出现内存可见性问题
正确同步的多线程程序:具有顺序一致性,JMM通过限制编译器和处理器重排序来为程序员提供内存可见性保证
未同步/未正确同步的多线程程序:
最小安全性保证:线程执行时读到的值,要么是之前线程写入的值(64位long或double可能只写入一半),要么是默认值
volatile内存语义增强:严格限制volatile变量和普通变量之间的重排序
final内存语义增强:保证final引用不会从构造函数内溢出
原文:https://www.cnblogs.com/kenshine/p/14520418.html