彻底学会Java并发编程——Java内存模型(JMM)

39 阅读4分钟

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。

线程安全单例方法

懒汉式:类加载导致该单实例对象被创建。

饿汉式:类加载不会导致该单实例对象被创建,首次使用该对象的时候才会被创建。