Volatile和JMM的爱恨情仇
什么是Java内存模型(JMM)
-
Java 语言为了保证并发编程中可以满足原子性、可见性及有序性,于是推出了一个概念就是 JMM 内存模型。
-
JMM 内存模型,目的是为了在多线程条件下,使用共享内存进行数据通信时,通过对多线程程序读操作、写操作行为规范约束,来尽量避免多次内存数据读取不一致、编译器对代码指令重排序、处理器对代码乱序执行等带来的问题。
-
JMM 内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
-
JMM 内存模型将内存主要划分为主内存和工作内存两种。规定 所有的变量都存储在主内存中,每条线程都拥有自己的工作内存,线程的工作内存中保存了该线程所需要用到的变量在主内存中的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读、写主内存。
-
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要线程自己的工作内存和主存之间进行数据交互。
JMM 内存模型工作内存、主内存和 JVM 内存有什么关系?
JMM 内存模型中,工作内存和主内存其实跟JVM内存的划分是在不同层次上进行的,是自己的一套抽象概念,大概可以理解为,主内存对应的是 Java 堆中的对象实例部分,而工作内存对应的则是栈中的部分区域。
JMM 定义了哪些操作来完成主内存和工作内存的交互操作?
JMM 定义了8 个操作来完成主内存和工作内存的交互操作:
-
首先是从 lock 加锁开始,作用于主内存的变量,把一个变量标识为一条线程独占的状态;
-
read 读取,作用于主内存变量,将一个变量的值从主内存读取到工作内存中;
-
load 加载,作用于工作内存的变量,把 read 读取到的值加载到工作内存的变量副本中;
-
use 使用,作用于工作内存的变量,把工作内存中变量的值传递给执行引擎使用,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作;
-
assign 赋值,作用于工作内存的变量,把从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作;
-
store 存储,作用于工作内存的变量,把工作内存中变量的值传送回主内存中,以便随后的 write 的操作;
-
write 写入,作用于主内存的变量,把 store 得到的值放入主内存的变量中;
-
最后是 unlock 解锁,把主内存中处于锁定状态的变量释放出来,流程到这一步就结束了。
当修改volatile变量时,JMM会把线程对应的工作内存中的共享变量值刷新到主内存中。
当读取volatile变量时,JMM会把该线程对应的工作内存置为无效,线程从主内存中读取共享变量值。
volatile关键字是如何保证有序性的?
为了性能优化,JVM会在不改变
数据依赖性
的情况下,允许编译器和处理器对指令序列进行重排序
,而有序性问题指的就是程序代码执行的顺序与程序员编写程序的顺序不一致,导致程序结果不正确的问题。而加了volatile修饰的共享变量,则通过内存屏障
解决了多线程下有序性问题。
屏障类型 | 说明 |
---|---|
LoadLoad Barries | 确保Load1数据的装载先于Load2以及后续装载指令的装载。 |
StoreStore Barries | 确保Store1数据刷新到内存先于Store2以及后续存储指令的存储。 |
LoadStore Barries | 确保Load1数据的装载先于Store2数据刷新到内存以及后续存储指令的存储。 |
StoreLoad Barries | 确保Store1数据刷新到内存先于Load2数据的装载以及后续装载指令的装载。 |
-
在每个volatile写操作的前面插入一个StoreStore屏障。
-
在每个volatile写操作的后面插入一个StoreLoad屏障。
-
在每个volatile读操作的后面插入一个LoadLoad屏障。
-
在每个volatile读操作的后面插入一个LoadStore屏障。