『JVM』什么是Java内存模型(JMM)?

参考:《深入理解Java虚拟机》周志明

一、概要

Java内存模型(Java Memory Model)也就是我们经常提到的JMM。它用来屏蔽各种硬件和操作系统的内存访问差异,从而实现让Java程序在各种平台下都能达到一致的内存访问效果。

正确而又深刻的理解Java内存模型,能够让我们更好地了解并发的内幕,理解底层原理,是深入了解Java并发编程的首要条件。

二、硬件的效率与一致性

计算机中的并发问题与虚拟机中的情况有很多相似之处,计算机对并发的处理方案对虚拟机的实现也有很大的参考意义,因此先来了解一下计算机的内存模型(这部分有基础的同学可以快速跳跃阅读)。

在计算机中,绝大多数的运算任务单靠处理器是无法完成的,经常要与内存进行交互(即I/O操作:读数据写数据)。而我们都知道,计算机处理器和存储设备之间的运算速度有几个数量级的差距,如果大量的进行I/O操作,处理器就会一直等待存储设备,导致处理器优势无法体现出来。

因此,在现代计算机中都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲,使处理器能够快速执行几乎无需等待,从而达到“高并发”的效果。

但是引入高速缓存会带来新的问题,即缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务都涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致。为了解决这个问题,需要各个处理器访问缓存时都遵循一些协议——缓存一致性协议(MSI、MESI等)。

『JVM』什么是Java内存模型(JMM)?

除了增加高速缓存之外,处理器还会对输入的代码进行乱序执行优化,来使处理器内部的运算单元能尽量被利用。处理器会在计算后将乱序的结果重组,保证该结果与顺序执行的结果是一致的。因此,代码的顺序性并不能保证执行的顺序性。在Java虚拟机的即时编译器中也有类时的指令重排序优化

三、Java内存模型

(一)、模型介绍

Java内存模型(JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果。JMM主要目的是定义程序各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。

JMM定义了线程和主内存之间的抽象关系:

  • 所有的变量(不包括局部变量和方法参数,这两者都是线程私有的)都存储在主内存中

    与上文计算机的主内存不同,这里的主内存在物理上是虚拟机内存的一部分

  • 每条线程拥有自己私有的工作内存,该工作内存存储了被该线程使用的变量的主内存副本。

  • 不同的线程无法直接访问对方工作内存中的变量,线程间的变量传递都要通过主内存。

    最后这两条就体现了内存间交互操作的要点,将在下一小节详细介绍

『JVM』什么是Java内存模型(JMM)?

这两张图片是Java内存模型的不同表达,下面这张可与计算机内存模型进行比较。

『JVM』什么是Java内存模型(JMM)?

JMM和我上一章所讲的Java内存区域并不是同一个层次的划分,两者几乎没有关系,最好不要混淆。

(二)、内存间交互操作

还记得上一小节抽象概念的最后两点吗?这里再赘述一遍:

  • 每条线程拥有自己私有的工作内存,该工作内存存储了被该线程使用的变量的主内存副本。
  • 不同的线程无法直接访问对方工作内存中的变量,线程间的变量传递都要通过主内存。

因此,如何实现主内存和工作内存的交互是一个重要的问题。JMM定义了8种操作指令来实现变量在主内存和工作内存间的拷贝,Java虚拟机必须保证每一种操作都是原子的、不可再分的(double和long类型除外)。

下面看似冗长,但实则句句精华,容易理解

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存,以便随后的load操作使用

  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

    如果要把一个变量从主内存拷贝到工作内存,必须要按顺序执行read和load操作,但不要求连续执行,可以插入其他指令。

  • use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作

  • assign(赋值)::作用于工作内存的变量,它把一个从执行引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作

  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存,以便随后的write操作使用

  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

    如果要把一个变量从工作内存同步回主内存,必须要按顺序执行store和write操作,但不要求连续执行,可以插入其他指令。

JMM还规定了在执行上述的8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况
  • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,也就是说对一个变量执行use、store操作之前,必须先执行执行assign和load操作
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
  • 如果对一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
  • 对一个变量执行unlock操作之前,必须先把此变量同步回内存(执行store、write操作)

上述的访问操作和规则限定,再加上下一节所讲的volatile的一些特性,就已经能够准确地描述出Java程序中那些内存访问操作在并发下才是安全的。

(三)、volatile型变量的特性

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。

当一个变量被定义成volatile之后,他将具有两项特性:

  • 保证此变量对所有的线程的可见性

    这里的“可见性”是指当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,即新值对于其他线程来说都是立即得知的

    普通变量的值在线程间传递均需要通过主内存来完成

  • 禁止指令重排序优化(保证有序性)

    通过编译可以得知,有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)。只有一个CPU访问内存时,并不需要内存屏障

    普通变量仅会保证在该方法执行的过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序和程序代码中的执行顺序一致

(四)、针对long和double型变量的特殊规则

在上一篇文章我们讲过,long和double型这两个64位的变量都很特殊,不管是在局部变量表中的变量槽还是在操作数栈的栈容量里,它们都要占据两个位置(其他变量只需要一个位置),这就难以保证long和double型变量读写操作的原子性,而上述的8条操作都要保证原子性。

因此,在JMM模型中定义了一条宽松的规定(long和double的非原子性协定):

允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性。

如果多线程情况下double或long类型并未声明为volatile,并且同时对它们进行读写操作,那么可能会出现某些线程读取到一个既不是原值,也不是被其他线程修改后的值。

虽然Java规范允许上面规定的实现,但商用虚拟机中基本采用了原子性操作,不会出现上面的异常情况,因此平常的使用不需要刻意将long和double型变量声明为volatile。

(五)、原子性、可见性与有序性

1、原子性(Atomicity)

JMM中直接保证原子性变量操作包括read、load、assign、use、store和write,可以大致认为,基本类型数据的访问、读写都是具有原子性的(除了long和double)。在Java代码中通常要用原子类(AtomicInteger、AtomicBoolean等)来实现原子性

如果应用场景需要更大范围的原子性保证,JMM还提供了lock和unlock操作来满足这种需求,但是虚拟机并没有将其直接开放给用户使用,而是提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作(在Java代码中就是同步块——synchronized关键字)。即在synchronized块之间的操作也具有原子性。

2、可见性(Visibility)

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

这点在volatile特性中就已经提过了,不止如此,在Java代码中synchronized和final也能实现可见性。

3、有序性(Ordering)

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性

volatile 是因为其本身包含“禁止指令重排序”的语义,而synchronized是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,此规则决定了持有同一个锁的两个同步块只能串行执行。

听起来synchronized是不是很万能,但是它本身还是有一定的性能影响的。

全文结。希望大家多多支持,点个赞就好,哈哈。

『JVM』什么是Java内存模型(JMM)?