概述
本章主要深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题。
1.Java内存模型
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
原子性-保证指令不会受到线程上下文切换的影响
可见性-保证指令不会受cpu缓存的影响
有序性-保证指令不会受cpu指令并行优化的影响
可见性
退不出的循环
先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
分析:1.初始状态,t线程刚开始从内存读取了run的值到工作内存
2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run 的访问,提高效率
3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
可见性与原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况︰上例从字节码理解是这样的:
比较一下之前我们将线程安全时举的例子∶两个线程一个i++一个i--,只能保证看到最新值,不能解决指令交错
5.3有序性
排序导致结果有问题
情况1︰线程1先执行,这时ready = false ,所以进入else分支结果为1
情况2∶线程2先执行num= 2,但没来得及执行ready= true,线程1执行,还是进入 else分支,结果为1
情况3∶线程2执行到 ready = true,线程1执行,这回进入if分支,结果为4(因为num已经执行过了)
情况4:线程2执行ready= true,切换到线程1,进入if分支,相加为О,再切回线程2执行num= 2,结果为0
这种现象叫做指令重排,是JIT编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:
解决方法
volatile修饰的变量,可以禁用指令重排
volatile原理
happens-before
happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见
线程对volatile的写,对接下来其他线程对该变量的读可见
线程start前对变量的写,对该线程开始后对该变量的读可见
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束)
线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
具有传递性,如果x hb-> y 并且y hb-> z 那么有x hb-> z ,配合 volatile 的防指令重排,有下面的例子
4.第五章小结
可见性-由JVM缓存优化引起
有序性-由JVM指令重排序优化引起
happens-before规则
原理方面
CPU指令并行
volatile
模式方面
两阶段终止模式的volatile改进
同步模式之balking