这一章我们进一步深入学习
共享变量
在
多线程
间的
【可见性】
问题与多条指令执行时的
【有序性】
问题
1. Java 内存模型
JMM
即 Java Memory Model,它定义了主存
、工作内存
抽象概念
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
2. 可见性
因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器
会将 run 的值缓存至自己工作内存
中的高速缓存中,减少对主存
中 run 的访问,提高效率
解决方法:volatile
(易变关键字):不能解决原子性,适合于只有一个写多个读的情况
它可以用来修饰成员变量
和静态成员变量
,他可以避免线程从自己的工作缓存
中查找变量的值,必须到主存中获取它的值
,线程操作 volatile 变量都是直接操作主存
synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低
volatile用在;一个线程的修改对另一个线程可见(对volatile修饰的变量可见)
重温终止模式之两阶段终止模式:
之前使用的是打断标记
,选择使用停止标记
同步模式之 Balking:一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
用在:当前端页面多次点击按钮调用 start 时
3. 有序性
指令重排:是 JIT 编译器在运行时的一些优化
口述:指令重排,其实是jit即时编译器对我们的代码做的优化,比如说我们写了两行代码,当然对于我们这个方法来说的话这两行代码谁先谁后其实互不影响的,那JIT可能就会做出优化,执行顺序上会改变,但是如果说我们的另一个方法要用到这两行代码定义的值时,在多线程环境下,就可能因为指令重排的影响进而导致出错。其实volatle就可以解决指令重排的,当然只针对一个线程中的代码来说,底层它用到了写屏障和读屏障,保证了写屏障之前的代码不会出现在写屏障后面,读屏障之后的代码不会出现在读屏障的前面,也就解决了指令重排导致的结果错误。
我们希望r1的值是1或者4,但是指令重排后可能出现0。
如何避免指令重排?
volatile 修饰的变量,可以禁用指令重排,volatile 之前的代码都不会重排。
3.1 volatile 原理
volatile 的底层实现原理是内存屏障
,Memory Barrier
- 对 volatile 变量的
写指令后
会加入写屏障
- 对 volatile 变量的
读指令前
会加入读屏障
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
还是那句话,不能解决指令交错:
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也
只是保证了本线程内相关代码不被重排序
下图中,t2线程的读在t1线程的写屏障前面,对这种指令交错。volatile不能解决,这是由cpu的时间片来决定的,volatile只能解决的是一个线程中的代码不能重排,不能保证线程间的这种另一个意义上的重排。
总结;
volatile解决了有序性和可见性,原子性不能保证。
synchronized都可以保证。