重新认识 Java 内存模型(JMM)

Posted 小羊子说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重新认识 Java 内存模型(JMM)相关的知识,希望对你有一定的参考价值。

通过学习《深入理解Java虚拟机》有关Java 内存模型的介绍,整理的学习笔记,供你参考。

文章目录

Java 内存模型定义

Java 内存模型 (Java Memory Model ),简称JMM屏蔽各种硬件和操作系统的内存访问的差异 性,以实现JAVA 程序在各个平台下都能达到一致的内存访问效果。

首先,引入《深入理解Java虚拟机》中的一张图,如下:

如图所示,在每一个线程中,都会有一块内部的工作内存(working memory)。这块内存保存了主内存共享数据的拷贝副本。

注意区分:JVM内存中有一块线程独享的内存空间 ----- 虚拟机栈,这里的的工作内存不是虚拟机栈。在Java线程中,不存在所谓的工作内存,它只是对CPU寄存器和高速缓存的抽象的描述。

主内存和工作内存

  • Java内存模型中规定了所有的变量都存储在主内存中。
  • 每条线程还有自己的工作内存,保存了该线程中使用的变量的主内存副本,线程对变量的所有操作(读取、赋值)都在工作内存中进行。
  • 不同的线程之间也无法直接直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
  • 主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的是工作内存。

CPU普及

做为Java程序员,应该都知道线程是CPU调度的最小单位,线程中的字节码指令最终都是在CPU中执行的。

为了“压榨”处理性能,达到“高并发”的效果,在CPU中添加了高速缓存(cache)为作为缓冲。

在执行任务时,CPU会先将运算所需要的数据copy到高速缓存中,让运算能够快速进行,当运算结束后,再将缓存中的结果刷回(flush back)主内存,这样CPU就不用等待主内存的读写操作了。

那么问题来了。每个处理器都有自己的高速缓存,同时又共同操作同一块内存,当多个处理器同时操作主内存时,可能导致数据不一致,这就是缓存一致性问题。

内存间交互操作

关于主内存和工作内存之间的交互协议说明:

内存交互操作有八种,虚拟机的实现保证每一个操作都是原子性的:

  1. lock(锁定):作用于主内存的变量,标识变量为线程独占状态
  2. unlock(解锁):作用于主内存的变量,释放一个处于锁定状态的变量,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存变量,从主内存中读取出后面load操作要用到的变量
  4. load(载入):作用于主内存中的变量,把刚才read的值放入工作内存的副本中
  5. use(使用):作用于工作内存中的变量,当线程执行某个字节码指令需要用到相应的变量时,把工作内存中的变量副本传给执行引擎
  6. assign(赋值):作用于工作内存中的变量,把一个从执行引擎中接受到的值放入工作内存的变量副本中
  7. store(存储):作用于工作内存中的变量,把工作内存中的变量送到主内存,给后续的write使用
  8. write(写入):作用于主内存中的变量,把store的工作内存中的变量值,写入主内存中

JMM对这八种指令的使用,制定了如下规则:

  • read和load、store和write必须顺序执行,而且两个指令绑定出现;就是说出现read就要有load
  • 不允许一个线程丢弃最近的assign操作,工作内存中的变量改变后,必须write同步到主内存
  • 不允许一个线程把没有发生assign操作的变量同步到主内存
  • 新的变量必须诞生于主内存,不允许工作内存使用一个没有初始化的变量;use、store操作变量之前,必须经过load和assign操作
  • 变量同一时刻只允许一个线程对其lock,该线程可以对该变量加锁多次,释放锁需要执行相同次数的unlock,lock和unlock要成对出现
  • 一个变量没有lock,不能unlock;并且一个线程不能unlock被其他线程锁住的变量
  • 执行unlock前,必须把工作内存中的变量同步到主内存中
  • 执行lock操作,需要清空工作内存(所有),并且需要使用该变量之前,要重新执行load和assign操作

对于volatile 型变量的特殊规则

对所有线程是立即可见的,对volatile变量所有的写操作都能够立即反映到其他线程之中。

换句话说,volatile变量在各个线程中是一致的。(正确)

基于以上论据,不能推出基于 volatile变量的运算在并发下是线程安全的。(需要注意)

原子性、可见性与有序性

见这篇介绍 链接:如何重新认识synchronized和volatile

先行发生(Happens-Before)原则

为什么有这个?

如果Java内存中的所有的有序性都靠 volatile 和synchronized,那么有很多操作都会变得啰嗦。

它是判断数据是否存在竞争,线程是否安全的非常有用的手段。通过这个原则,我们可以通过几条简单的规则一揽子解决并发环境下两个操作之间是否可能存在 冲突的所有问题。

下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。 如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序:

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。 准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、 循环等结构。

  2. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。 这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

  3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

  5. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、 Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

JVM内存区域的JMM的区别

JVM内存区域是指JVM运行时将内存数据分区域存储,强调对内存空间的划分。

JAVA内存模型是JAVA语言在多线程并发情况下对于共享变量内存操作的规范:解决变量在多线程的可见性、原子性的问题。

参考资料:

《深入理解Java虚拟机》周志明 著

以上是关于重新认识 Java 内存模型(JMM)的主要内容,如果未能解决你的问题,请参考以下文章

JAVA内存模型(JMM简述)

JMM java内存模型

Java并发编程-JMM内存模型与volatile关键字

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)