jmm的内存可见性
jmm的内存可见性
Java内存模型jmm
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM中的主内存
- 存储java实例对象
- 包括成员变量、类信息、常量、静态变量等
- 属于数据共享的区域,多线程并发操作时会引发线程安全问题
JMM中的工作内存
- 存储当前方法的所有本地变量信息,本地变量对其他线程不可见
- 变量会从主内存拷贝到工作内存,每个线程只能访问自己的工作内存
- 其内还包括字节码行号指示器,Native方法信息。
- 属于线程私有数据区域,不存在线程安全问题
JMM与Java内存区域
JMM与Java内存区域划分是不同的概念层次
- JMM描述的是一组规则,围绕原子性,有序性,可见性展开,控制程序中各个变量在共享数据区域和私有数据区域的访问方式
- 相似点:存在共享区域和私有区域
- JMM中主内存是共享区域(在Java内存区域中应该包括堆和方法区),工作内存是私有区域(在Java内存区域中应该包括程序计数器、虚拟机栈和本地方法栈)
主内存与工作内存的数据存储类型及操作方式
- 方法里的基本数据类型本地变量(局部变量)将直接存储在工作内存的栈帧结构中(如boolean,byte,char,int等)。
- 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中(引用在栈帧,实例在堆中)。
- 成员变量、static变量、类信息均会被存储在主内存中。
- 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回住内存。
JMM如何解决可见性问题
把数据从内存加载到缓存、寄存器,运算结束后写回主内存
如此在多线程的时候会带来数据一致性问题
JMM为了数据一致性会进行指令重排序
需要满足的条件
- 在单线程环境下不能改变程序运行的结果
- 存在数据依赖关系的不允许重排序
- 无法通过happens-befor原则推导出来的,才能进行指令的重排序
happens-befor
A操作的结果需要对B操作可见,则A与B存在happeds-befor关系
例如:
1 | i = 1; //线程A执行 |
上面两行语句就存在happens-befor关系
八大原则
1、单线程happen-before原则:
- 在同一个线程中,书写在前面的操作happen-before后面的操作。
2、锁的happen-before原则:
- 同一个锁的unlock操作happen-before此锁的lock操作。
3、volatile的happen-before原则:
- 对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
4、happen-before的传递性原则:
- 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
5、线程启动的happen-before原则:
- 同一个线程的start方法happen-before此线程的其它方法。
6、线程中断的happen-before原则:
- 对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
7、线程终结的happen-before原则:
- 线程中的所有操作都happen-before线程的终止检测。
8、对象创建的happen-before原则:
- 一个对象的初始化完成先于他的finalize方法调用。
如果两个操作不满足上述任意一个规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。
如果操作A happen-befor操作B,那么操作A在内存上所作的操作对操作B都是可见的。
示例:
1 | private int value = 0; |
如果线程A执行write
操作,线程B执行red
操作,且线程A优先于线程B执行
结果:这段代码均不满足happen-befor的条件,不是线程安全
修改方法:满足2:加入volatile修饰符或者满足3:对操作加锁
Volatile:JVM提供的轻量级同步机制
- 保证被Volatile修饰的共享变量对所有线程总是可见的
- 禁止指令重排序优化
Volatile的可见性
被Volatile修饰的变量对所有线程总是立即可见的,对Volatile变量的所有写操作总是能立即反映到其他线程中。但是对Volatile的运算在多线程环境中并不保证安全性
例如:
1 | public class VolatileVisibility { |
value变量的任何改变都会立即反映到线程中,但如果多个线程同时调用increace()
方法就会出现线程安全问题,value++不具备原子性。
Volatile为什么立即可见
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中。
当读取volatile变量时,JMM会把该线程对应的工作内存置为无效。
Volatile如何禁止重排优化
内存屏障(Memory Barrier)
是一个CPU指令,其作用有两个
- 保证特定操作的执行顺序
- 通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
- 保证某些变量的内存可见性
- 强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
经典单例双重检测实现:
线程安全的单例写法:
1 | public class Singleton { |
上述代码仍然又问题,在第一次检测时候可能读到instance
不为空但是其还没彻底创建出来。
对象创建流程:
因为其不存在happen-befor,故这三条的顺序可能变成如下:
所以instance需要加volatile
进行修饰,禁止其重排序