JMM定义了主存和工作内存的抽象概念。底层是cpu寄存器、缓存、硬件内存、cpu指令优化等。
JMM体现在三个性质:原子性(保证指令不会受到上下文切换的影响)、可见性(保证指令不会收到cpu缓存的影响)、有序性(保证指令不会受到cpu指令优化的影响)。
主存:共享信息存储的位置。工作内存:每个线程私有的信息存储的位置。
可见性
发生可见性问题的原因
main线程对run变量的修改对于t线程不可见。
原因:
初始状态,t线程从主存中读取了run的值到工作内存:
因为要频繁读run值,JIT编辑器把run值缓存到自己工作内存中的高速缓存中。
此时主存中改变了run的值,但工作内存使用的是自己缓存中的run的值。
可见性的解决方法
1、 用volatile
2、用synchronized也可以保证可见性:只不过是重量操作。
volatile不能解决指令交错问题,所以无法解决原子性。
有序性
指令重排
JVM会在不影响正确性的情况下调整语句的执行顺序
但是多线程下,指令重排会影响正确性。
cpu支持多级指令的流水线,同时执行取指令、指令编码、指令执行、内存访问、数据写回五个操作,提高吞吐量。
如上图所示,指令的不同阶段可以通过重排序和组合来实现指令级并行。
指令重排前提是不能影响结果。
因为存在指令重排,这种情况下r1的值会有3种结果。
因为结果是0的情况很少,可以使用java并发压测工具jcstress来测试。
禁用指令重排
变量前面加上volatile:阻止该变量之前的代码进行重排序。
volatile原理
保证可见性
volatile实现内存屏障。
在volatile变量的写指令后会加入写屏障。写屏障保证在该屏障之前的共享变量的改动都同步到主存。
在volatile变量的读指令前会加入读屏障。读屏障保证在该屏幕之后的共享变量的读取,读的都是主存中最新的数据。
保证有序性
写屏障会在重排序的时候会保证volatile变量前面的代码不会排到后面去。
读屏障会在重排序的时候会保证volatile变量后面的代码不会排到前面去
但是volatile不能解决指令交错的问题,因为他只能保证线程内部指令的顺序。
写屏障只能保证后面的读读到的是最新的结果,不能保证跑到他前面去。
double-checked locking问题(dcl)
等同于:
synchronized代码块里的指令可以被重排序。
例如在这段代码里,灰色部分可以被重排序。
他对应下图17-24的指令。
关键在于上面的0部指令,getstatic可以越过monitor读取instance变量的值。
此时t1还没将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2得到的就是一个没有初始化完成的单例。
解决dcl问题
对instance使用volatile指令可以禁止重排。
原理如下:
写屏障之前的代码不能排在putstatic之后。
happens-before规则
happens-before规则保证一个线程的写操作是别的线程的读操作可见的。
情况1:
两个线程都加锁了synchronized(m),第一个线程的写x=10 对第二个线程的读 sout(x)是可见的。
线程解锁m之前对变量的写,对于接下来对m加锁的线程对该变量的读可见。
情况2
一个线程对volatile变量的写操作,对于其他线程对该变量的读可见。
情况3
线程start前对变量的写,对于该线程开始后对该变量的读是可见的。
情况4
线程开始后对变量的写,对于得知该线程结束后的其他线程的读是可见的。(比如其他线程调用t1.isAlive()或t1.join()等待他结束)
情况5
在t1打断t2前对变量进行写,对于其他线程知道了t2被打断后的读可见。
情况6
对变量默认值(0,null,false)的写,对于其他线程对于该变量的读可见。
情况7
这种volatile的情况也可见
volatile用在一个线程写,别的线程读的情况。
还有就是dcl问题用volatile。
线程安全单例方法
懒汉式:类加载导致该单实例对象被创建。
饿汉式:类加载不会导致该单实例对象被创建,首次使用该对象的时候才会被创建。