JUC高并发编程-共享模型之内存模型(JMM)
内存模型(JMM)
Java内存模型
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
- 原子性-保证指令不会受到线程上下文切换的影响
- 可见性-保证指令不会受到cpu缓存的影响
- 有序性-保证指令不会受cpu指令并行优化的影响
可见性
退不出的循环
先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
1 | static boolean run = true; |
分析:
- 初始状态,t线程刚开始从主存读取了run的值到工作内存。
因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己g工作内存中的高速缓存中,减少对主存中run的访问,提高效率。
1秒后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
解决方法
1 | volatile static boolean run = true;//加入volatile易变关键字 |
它可以用来修饰成员变量和静态变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
可见性和原子性
前面的例子体现的实际是可见性,他保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。
注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对更低。
有序性
JVM会在不影响正确性的前提下,可以调整语句的执行顺序。
1 | static int i; |
以上执行i,j的顺序不会影响最后结果。这种特性称之为指令重排,多线程下指令重排会影响正确性。
指令重排原理
鱼罐头的故事
加工一条鱼需要50分钟,只能一条鱼 ,一条鱼顺序加工。。。。
可以将每个鱼罐头的加工流程细分为5个步骤:
- 去鳞清洗 10分钟
- 蒸煮沥水 10分钟
- 加注汤料 10分钟
- 杀菌出锅 10分钟
- 真空封罐 10分钟
即使只有一个工人,最理想的情况:他能够在10分钟内同时做好5件事,因为对第一条鱼的真空装罐,不会影响对第二条鱼的杀菌出锅。
指令重排序列优化
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为:取指令-指令译码-执行指令-内存访问-数据写回这5个阶段。
volatile原理
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对volatile变量的写指令后会加入写屏障
- 对volatile变量的读指令后会加入读屏障
如何保证可见性
写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存中
1 | public void actor2(I_Result r){ |
而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新的数据
1 | public void actor1(I_Result r){ |
如何保证有序性
写屏障确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
1 | public void actor2(I_Result r){ |
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
1 | public void actor1(I_Result r){ |
不能解决指令交错:
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证知识保证了本线程内相关代码不被重排序
double-checked locking问题
以著名的double-checked locking单例模式为例
1 | public final class Singleton{ |
以上实例实现的特点:
- 懒惰实例化
- 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁
- 有隐含的,但很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外
happens-before
happens-before规定了共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结,抛开一下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其他线程对该共享变量的读可见。
线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见。
1 | static int x; |
线程对volatile变量的写,对接下来其它线程对该变量的读可见
1 | volatile static int x; |
线程start前对变量的写,对该线程开始后对该变量的读可见
1 | static int x; |
线程结束前对变量的写,对其它线程得知它的结束后的读可见(比如其它线程调用t1.isAlive()或t1.join())等
1 | static int x; |
线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)
1 | static int x; |
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
具有传递性,如果x hb->y并且y hb->z那么有x hb->z,配合volatile的防指令重排,有下面的例子
1 | volatile static int x; |