JVM 知识 -- JMM

本文最后更新于:5 天前

JMM 结构

JMM

(再看看和 CPU 的结构像不像)

CPU 结构

存储数据时

JMM 的工作空间主要存放私有数据基本数据类型

JMM 的共享空间主要存放线程共享数据

修改数据时

当线程修改私有数据时,直接在工作内存修改

当线程1 和线程2 修改他俩共享的数据时,需要先从主内存将数据复制到工作内存中,修改完成后,需要再刷入共享内存中

JMM 解释

JMM (Java Memory Model) 定义了 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM 是整个计算机虚拟模型,所以 JMM 隶属于 JVM

Java 虚拟机规范中试图定义一种 Java 内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现 Java 程序在各种平台下都能达到一致的内存访问效果

JMM 主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节

JMM 决定一个线程对共享变量的写入何时对另一个线程可见

JMM 即 Java 线程之间的通信是采用共享内存模型

Java 内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步

这里的内存模型是指,在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象

JMM 是抽象概念

JMM 是一个抽象概念,并不真是存在,但是无论是工作空间还是共享空间,都包括了真实的 CPU cache、寄存器和机器本身的内存等

  • 这样设计的好处是:

    保证数据的隔离性,如果没有工作空间,一个线程读一个线程写,或者多个同时写入,就更容易数据不一致。

  • 带来的缺点:

    线程访问共享变量时,线程不安全

    一个线程已经写完但是还没来得及刷入到内存中,但是另一个线程已经从内存中读入了原有的数据,会导致读到了脏数据

JMM 问题和目标

当对象和变量可以存储在计算机中不同的内存区域时,会出现两个主要问题:

  1. 线程更新(写)到共享变量的可见性

  2. 读取,检查和写入共享变量时的竞争条件

JMM 是围绕并发过程中如何处理可见性,原子性和有序性这3个特征建立起来的

  1. 可见性: volatile,synchronized,final

  2. 原子性: 一个操作或多个操作,要么全部执行不被打断,要么都不执行

  3. 有序性: 计算机在执行程序时,为了提高性能,会进行指令重排(包括: 编译器优化重排,指令并行重排,内存系统重排)

    1. as-if-serial: 即在指令重排时,无论怎么优化,单线程程序执行程序的结果不会改变

    2. happens-before

      在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系

      1. 程序顺序规则: 一个线程中的每个操作,happens-before 于线程中的任意后续操作

      2. 监视器锁规则: 一个锁的解锁,happens-before 于随后对这个锁的加锁

      3. volatile 规则: 对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读

      4. 传递性: 如果 A happens-before B,且 B happens-before C,那么 A happens-before C

      特别注意:

      两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!

      happens-before 仅仅要求前一个操作执行的结果对后一个操作可见,且前一个操作按顺序排在第二个操作之前

      最后一句话的意思是,前一个操作可能后被执行,但是前一个操作的结果一定先于后一个结果之前被后一个操作看到,前一个操作也可以比后一个操作先或者后执行,但是一定是前一个操作先出结果

    这三个特征底层实现都是通过内存屏障实现的

JMM 和 JVM 的区别和联系

  • 联系:

    JVM 就是按照 JMM 来划分的,定义了哪些是共享区域(共享空间)哪些是线程私有区域(工作空间)

  • 区别:

    JMM 是规范,是抽象模型,并不真实存在

    而 Java 中的内存区域可能是内存也有可能是 CPU 的高速缓存,是具体存在的

JMM 具体如何工作的

不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成。一个变量如何从主内存拷贝到工作空间,又如何从工作空间同步回共享空间,JMM定义了一下8种操作来完成,虚拟机实现时必须保证每一个操作的原子性。

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

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

  • read(读取) 作用于主内存的变量,将变量的值从主内存传输到线程的工作内存,以便后面的load操作。

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

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

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

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

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

JMM 中的具体操作

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

  1. 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

  2. 不允许read和load、store和write操作之一单独出现

  3. 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。

  4. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。

  5. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

  6. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现

  7. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

  8. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

  9. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

JMM 和 MESI 的区别

有了MESI,为什么还需要JMM 及 volatile 关键字?

  1. 并不是所有的硬件架构都提供了相同的一致性保证,不同的硬件厂商实现不同,JVM需要volatile统一语义。

  2. 可见性问题不仅仅局限于CPU缓存内,JVM自己维护的内存模型(JMM)中也有可见性问题。使用volatile做标记,可以解决JVM层面的可见性问题。