Java内存模型
Java内存模型
概述
JMM即 Java Memory Model,它定义了主存、工作内存抽象概念。
底层对应着复杂的优化:
- CPU 增加了缓存,以均衡与内存的速度差异;
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。
引发的问题体现在以下几个方面:
- 原子性:保证指令不会受到线程上下文切换的影响
- 可见性:保证指令不会受cpu缓存的影响
- 有序性:保证指令不会受cpu指令并行优化的影响
原子性
原子性是指一个操作或者多次操作是不可中断的。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
例如:两个线程对同一个变量进行修改。
1 | public class 原子性 { |
问题:出现错误数据
解决:对临界区代码进行加锁(也是我们之前一直通过synchronized和各种Lock解决的事情)。
可见性
由于cpu缓存的影响,有时候线程对变量的修改,并不会让其他线程看见。
例如:
1 | static boolean run = true; |
原因:
Java内存主要是分为主内存和线程独有的工作内存;
- 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
- 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
- 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决:
给需要保证可见性的变量加上volatile(易变关键字,只能保证可见性,它最原始的意义就是禁用 CPU 缓存)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
volatile的底层实现原理是内存屏障,Memory Barrier (Memory Fence)
- 对volatile变量的写指令后会加入写屏障
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 对volatile变量的读指令前会加入读屏障
- 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
能够保证的有序性也正是,写屏障会让指令不会出现在屏障后面,读屏障会让指令不会在屏障前面。但是不能解决指令的交错问题还是得用synchronized等解决。
- 对volatile变量的写指令后会加入写屏障
- 用synchronized锁住需要保证可见性的变量代码,因为synchronized同样可以保证变量的可见性,但比较重量级(例如使用System.out.println()同样也可以保证可见性,因为底层也是synchronized)
happens-before规则
happens-before规定了对共享变量的写操作对其它线程的读操作可见,即前面的一个操作的结果对后续操作是可见的,它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。
- synchronized锁中对共享变量的读写是能够保证可见性的。
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见。
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见。
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待
它结束) - interrupted打断之前对共享变量的修改是对其他线程可见的。
- 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
- 具有传递性,volatile变量赋值前其他变量的赋值也会被同步到主存中,对于其他线程也是可见的。
有序性
JVM会在不影响正确性的前提下,可以调整语句的执行顺序。这种特性称之为『指令重排』,但是多线程下『指令重排』会影响正确性。
在多线程下的指令重排序机制问题:
使用两个线程执行两种方法
1 | int num = 0 |
如果JVM进行指令重排序,让actor2的两条语句颠倒,就会得到r.r1=0的错误结果。
解决:给禁止重排序的变量加上volatile,这样在该变量上面的代码就不会重排序。