本篇总结的是 JMM内存模型,volatile 关键字保证有序性和可见性的原理,happens-before原则。
参考文章:Java面试官告诉你JMM是什么和面什么、阿里实习面经、「阿里面试系列」分析Synchronized原理,让面试官仰望、happens-before理解和应用、面试官:说说什么是 Java 内存模型(JMM)?、面试官:volatile是如何保证可见性和有序性的?、happen-before原则、通俗易懂讲解happens-before原则
1、什么是Java内存模型?
首先,要知道,Java 内存模型指的是 JMM,而不是运行时数据区哦~
Java 语言为了保证并发编程中可以满足原子性、可见性及有序性,于是推出了一个概念就是 JMM 内存模型。
JMM 内存模型,目的是为了在多线程条件下,使用共享内存进行数据通信时,通过对多线程程序读操作、写操作行为规范约束,来尽量避免多次内存数据读取不一致、编译器对代码指令重排序、处理器对代码乱序执行等带来的问题。
JMM 内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
JMM 内存模型将内存主要划分为主内存和工作内存两种。规定 所有的变量都存储在主内存中,每条线程都拥有自己的工作内存,线程的工作内存中保存了该线程所需要用到的变量在主内存中的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读、写主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要线程自己的工作内存和主存之间进行数据交互。
如图所示:
JMM 内存模型工作内存、主内存和 JVM 内存有什么关系?
JMM 内存模型中,工作内存和主内存其实跟JVM内存的划分是在不同层次上进行的,是自己的一套抽象概念,大概可以理解为,主内存对应的是 Java 堆中的对象实例部分,而工作内存对应的则是栈中的部分区域。
2、JMM 定义了哪些操作来完成主内存和工作内存的交互操作?
JMM 定义了8 个操作来完成主内存和工作内存的交互操作:
① 首先是从 lock 加锁开始,作用于主内存的变量,把一个变量标识为一条线程独占的状态;
② read 读取,作用于主内存变量,将一个变量的值从主内存读取到工作内存中;
③ load 加载,作用于工作内存的变量,把 read 读取到的值加载到工作内存的变量副本中;
④ use 使用,作用于工作内存的变量,把工作内存中变量的值传递给执行引擎使用,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作;
⑤ assign 赋值,作用于工作内存的变量,把从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作;
⑥ store 存储,作用于工作内存的变量,把工作内存中变量的值传送回主内存中,以便随后的 write 的操作;
⑦ write 写入,作用于主内存的变量,把 store 得到的值放入主内存的变量中;
⑧ 最后是 unlock 解锁,把主内存中处于锁定状态的变量释放出来,流程到这一步就结束了。
如图所示:
JMM 基本可以说是围绕着在并发中如何处理这三个特性而建立起来的,也就是原子性、可见性、以及有序性。
3、volatile关键字是如何保证可见性的?
当一个共享变量被 volatile 修饰时,它会保证修改的值会被立即更新到主内存中,当有其他线程读取该值时,也不会直接读取工作内存中的值,而是直接去主内存中读取。
而普通的共享变量不能保证可见性的,因为普通共享变量被修改后,写入了工作内存中,什么时候写入主内存其实是不可知的,当其他线程去读取是,此时无论是工作内存还是主内存,可能还是原来的值,因此无法保证可见性。
被volatile关键字修饰的变量,在每个写操作之后,都会加入一条store内存屏障命令,此命令强制将此变量的最新值从工作内存同步至主内存;在每个读操作之前,都会加入一条load内存屏障命令,此命强制从主内存中将此变量的最新值加载至当前线程的工作内存中。
4、volatile关键字是如何保证有序性的?
volatile 可以禁止指令重排,保证程序会严格按照代码的先后顺序执行。
加了volatile 修饰的共享变量,通过内存屏障解决多线程下的有序性问题。原理如下:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障
在每个 volatile 写操作的后面插入一个StoreLoad屏障
在每个 volatile 读操作的后面插入一个LoadLoad屏障
在每个 volatile 读操作的后面插入一个LoadStore屏障
volatile 在写操作前后插入了内存屏障后生成的指令序列示意图如下:
volatile
在读操作后面插入了内存屏障后生成的指令序列示意图如下:
5、happens-before原则
happens-before 的概念:
JMM 可以通过 happens-before 关系向程序员提供跨线程的内存可见性保证(如果 A 线程的写操作 a 与 线程 B的读操作 b 之间存在 happens-before 关系,尽管 a 操作和 b 操作在不同的线程中执行,但 JMM 向程序员保证 a 操作将对 b 操作可见)。
happens-before 具体定义:前面一个操作的结果对后续操作是可见的。
1)如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。只要两个操作在指令重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序也是合法的。
happens-before 的 8 大原则:
① 单线程happen-before原则:同一个线程中前面的所有写操作对后面的操作可见。
② 锁的happen-before原则:同一个锁的 unlock 操作 happen-before 此锁的 lock 操作。这条规则是指对一个对象的解锁 happen-before 于后续对这个对象的加锁。
③ volatile的happen-before原则:对一个 volatile 变量的写操作 happen-before 对此变量的任意操作(当然也包括写操作了)。
④ happen-before的传递性原则:如果 A 操作 happen-before B 操作,B 操作 happen-before C操作,那么 A 操作happen-before C 操作。
⑤ 线程启动的happen-before原则:同一个线程的 start() 方法 happen-before 于此线程的其它方法。
⑥ 线程中断的happen-before原则:线程 A 新写入的所有变量,当调用 Thread.interrupt(),被打断的线程 B,可以看到 A 的全部操作。
理解:线程 A 写入的所有变量,调用 Thread.interrupt(),被打断的线程 B,可以看到 A 的全部操作。
⑦ 线程终结的happen-before原则:线程中的所有操作都 happen-before 线程的终止检测。
⑧ 对象创建的happen-before原则:一个对象的初始化完成(构造函数执行结束)先于他的 finalize() 方法调用。