深入理解Java虚拟机之----Java内存模型

Java 内存模型

1、硬件的效率与一致性

计算机的存储设备(主内存)的读写速度远远慢于处理器的运算速度(差好几个数量级),所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步会内存之中,这样处理器就无须等待缓慢的内存读写了。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享一个主内存(Main Memory),如下图所示。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,那么,同步回主内存应该以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有 MSI、MESI、MOSI、Synapse、Firefly 及 Dragon Protocol 等。本文中的 “内存模型“ 一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。

《深入理解Java虚拟机之----Java内存模型》

2、Java 内存模型(Java Memory Model,JMM)

Java 内存模型就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的规范。

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量指的是共享变量,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数。

Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下图所示。

《深入理解Java虚拟机之----Java内存模型》

如果要将此处的主内存、工作内存与虚拟机运行时数据区域作一个对应的话:主内存主要对应于方法区和堆,而工作内存则对应于虚拟机栈中的部分区域。大致如下图所示:

《深入理解Java虚拟机之----Java内存模型》

3、内存间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义了以下八种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • load(载入):作用于工作内存的变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 操作使用。
  • write(写入):作用于主内存的变量,把 store 操作从工作内存中得到的变量的值传放入主内存的变量中。

Java 内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 如果要把一个变量从主内存中复制到工作内存,就需要顺序地执行 readload 操作,如果要把变量从工作内存中同步回主内存中,就要按顺序地执行 storewrite 操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。即中间是可以插入其他指令的。
  • 不允许 readloadstorewrite 操作之一单独出现。
  • 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中 “诞生” ,不允许在工作内存中直接使用一个未被初始化( loadassign )的变量。即就是对一个变量实施 usestore 操作之前,必须先执行过了 assignload 操作。
  • 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 loadassign 操作初始化变量的值。
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 storewrite 操作)。

这八种内存访问操作及上述规定限定,再加上对 volatile 的一些特殊规定,就已经完全确定了 Java 程序中哪些内存访问操作在并发下是安全的。

4、原子性、可见性和有序性

Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这 3 个特征来建立的,下面我们来看一下哪些操作实现了这 3 个特性。

1)原子性:

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着 “同生共死” 的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

由 Java 内存模型来直接保证的原子性变量操作包括 readloadassignusestorewrite,我们大致可以认为基本数据类型的访问读写是具备原子性的( long 和 double 的非原子性协定除外)。另外就是虽然虚拟机未把 lockunlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorentermonitorexit 来隐式地使用这两个操作,反映到 Java 代码中就是同步块—- synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。

2)可见性:

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

volatile 的特殊规则(关于 volatile 的特殊规则可以看一下这篇文章:深入理解Java虚拟机之—-volatile关键字)保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,volatile 保证了多线程操作时变量的可见性。

除了 volatile 之外,Java 还有两个关键字能实现可见性,即 synchronizedfinal。同步块的可见性是由 “对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 storewrite 操作)“ 这条规则得到的,而 final 关键字的可见性在于:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 “ this “ 的引用传递出去,那在其他线程中就能看到 final 字段的值。

3)有序性:

Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。

Java 语言提供了 volatilesynchronized 两个关键字来保证线程之间操作的有序性,volatile 关键字本身包含了禁止指令重排序的语义,而 synchronized 则是由 “一个变量在同一时刻只允许一条线程对其进行 lock 操作“ 这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。但是需要注意的是:这里的有序指的是拥有同一个锁的两个同步块串行有序,但是每个同步块内部是可能发生指令重排序的。

5、先行发生( happens-before )原则

Java 语言中,除了 volatilesynchronized 两个关键字之外,还有另一个原则来完成 Java 内存模型中的有序性—- “先行发生“ 原则。

先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先行发生于操作 B,其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,“影响“ 包括修改了内存中共享变量的值、发送了消息、调用了方法等。

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

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是同一个锁,而 “后面“ 是指时间上的先后顺序。
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的 “后面“ 同样是指时间上的先后顺序。
  • 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupt() 方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
  • 传递性(Monitor Lock Rule): 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,就可以得到操作 A 先行发生于 操作 C。

注意:

先行发生原则仅仅要求前一个操作(的执行结果)对后一个操作可见,而没有要求前一个操作必须要在后一个操作之前执行。例如:操作 A 先行发生于 B,但操作 A 和操作 B 之间不存在数据依赖性,则可能会进行重排序,使得操作 B 在操作 A 之前执行。即:时间先后顺序与先行发生原则之间基本没有太大的关系,在衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

总结一下:由于主内存与处理器(CPU)速度上的不均衡,虚拟机在两者之间添加了一层工作内存(高速缓存)来提高程序的运行速度,而工作内存的存在会引入缓存一致性的问题,为了解决这个问题,引入了 Java 内存模型(JMM)。JMM 定义了程序中各个变量的访问规则,线程、主内存及工作内存三者之间的交互,线程与线程之间如何通信等规范,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

参考资料:

(1)《深入理解java虚拟机》周志明 著.

(2)三大性质总结:原子性,有序性,可见性

(3)Java内存模型(JMM)总结

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/haihui_yang/article/details/81988460
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注