JMM语义与重排
# JMM语义与重排
# 1.内存模型基础
顺序一致性模型(内存屏障限制最多)+重排序并行优化=JMM
- Java并发采用共享内存模型,线程之间的通信是根据写-读内存中的公共状态进行隐式通信。
- Java源程序在编译执行的过程,可能会经历几种重排序。JMM通过插入内存屏障指令(禁止前后指令重排),保证一致的内存可见性。
🔥无论是哪种内存模型,添加的内存屏障和约束越少,那么指令的并行度越高,性能越好;但是相对的指令代码执行的顺序不可控,易编程性变差。
# 2.重排序
代码在执行时,编译器和处理器可能都会对其进行重排序。
JMM会制定相应的规则,限制不同情况下的重排。
结论:多线程下,对数据依赖和控制依赖的操作进行重排序会改变执行结果。
# as-if-serial语义
保证单线程执行结果不会改变。换句话说,编译器和处理器重排时,不能改变存在数据依赖关系的操作。(单线程下重排控制依赖操作不受影响)
# 3.volatile内存语义
volatile具有以下特性和语义:
- 可见性:其它线程总能读到volatile变量最新的值
- 原子性:对单个volatile变量的操作具有原子性。每次读/写操作等效于使用一个synchronized锁同步。
- volatile写:在线程内存空间对当前volatile变量修改后,刷新到主内存共享区域。
- volatile读:线程内存空间的volatile变量置为无效,从主内存读取最新共享变量。
结论:为了实现volatile内存语义,通过插入不同内存屏障实现代码的同步。
# 4.锁的内存语义
ReentrantLock分为公平锁和非公平锁,它们的锁获取-释放的区别如下,分别代表两种锁的实现:
- 公平锁锁获取时,会读volatile变量;释放锁时会写volatile变量。——利用volatile的内存语义。
- 非公平锁在获取锁时,会使用CAS方法更新state值。
# 5.final内存语义
final域重排序与内存屏障限制:
- 写操作:final变量的写只能在构造函数内进行,不能重排到构造函数之外。
- 读操作:①final变量的对象引用的读操作②final变量的读操作。两者执行顺序不能改变
# 6.happens-before关系
happens-before
描述代码编写的先后关系,强调前面的操作结果一定对后面操作结果可见。
代码之间只要不存在数据依赖关系,那么代码实际执行顺序并不一定跟happens-before关系保持一致。
常见的happens-before规则:
- 程序顺序规则
- volatile规则:volatile变量写 —> happens-before —> volatile变量读。保证所有线程能够读到最新的共享变量。
- 监视器锁规则:对一个锁的解锁—>happens-before—>对一个锁的加锁。解锁后锁的状态要对全局可见,才能够加锁。
- 传递性
- start规则:A线程执行ThreadB.start启动线程,那么在执行ThreadB.start()之前对共享变量所做的修改,接下来在B线程开始执行后都是可见的。
- join规则:A线程执行ThreadB.join终止B线程,那么B线程终止前修改的共享变量,在A执行ThreadB.join都能够读取到。
# 7.延迟初始化方案
延迟初始化:Java程序中,有时候对象初始化的比较大,需要在使用该对象的时候再进行初始化,也就是懒加载,因此程序员需要延迟初始化。
public static Instance getInstance(){
if(instance==null)
instance=new Instance();
return instance;
}
2
3
4
5
但上述代码在多线程的环境下存在问题。
# Double-Checked Locking存在的问题
上述代码在多线程下存在问题,如果在方法加锁synchronized,虽然问题可以解决,但是加锁开销较大。
我们希望如果当前对象已经初始化,那么直接返回对象,不需要重新获取锁资源,从而减小锁粒度和开销,因此就有了下面的代码:
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
} // 8
} // 9
return instance; // 10
} // 11
}
2
3
4
5
6
7
8
9
10
11
12
然而上述代码存在的问题在于,第七行代码并不是一个原子操作,它分为两个步骤①分配内存地址空间,并完成初始化②instance引用指向内存空间。在多线程下若发生重排,则线程B在执行第4行进行判断时,会出现instance不为null,但是还没完成初始化的情况。
解决方案分为两个:
- ✨保证instance=new Instance()初始化过程中,不出现重排。
- 直接将instance变量设置为volatile修饰。单个volatile操作具有原子性。
✨instance=new Instance()初始化的过程对外不可见。
- 基于类初始化的方案:在执行类初始化期间,JVM会去获取一个锁,它可以同步多个线程同步过程,主要是类初始化锁的五个阶段...
public class InstanceFactory { private static class InstanceHolder { public static Instance instance = new Instance(); } public static Instance getInstance() { return InstanceHolder.instance ; //这里将导致InstanceHolder类被初始化 } }1
2
3
4
5
6
7
8