Java 内存模型(JMM)

60 阅读8分钟
  • JMM 是一个抽象的概念,定义了 Java 程序中对变量的读写方式,屏蔽了不同硬件和操作系统的内存访问差异,保证 Java 程序在不同的平台都能达到一致的访问效果
  • JMM 规定了所有的变量都存储在主内存中。每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

1、主内存与工作内存交互操作

JMM主内存与工作内存操作流程图.png

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

2、主内存与工作内存交互操作的基本规则

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

3、保证并发三大特性

(1)原子性

  • 原子性指的是一个操作是不可中断的

  • 保证原子性

    • 自带原子性保证

      • 在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作
    • synchronized

      • synchronized 可以保证边界操作结果的原子性
      • synchronized 可以防止多个线程并发的执行同一段代码,从结果上保证原子性
    • Lock

      • Lock 锁保证原子性的原理和 synchronized 类似
    • 原子类

      • 原子类的底层是使用 CAS 机制,CAS 机制保证了整个赋值操作是原子的不能被打断

(2)可见性

  • 可见性指的是当一个共享变量被一个线程修改后,其他线程能够立即感知到

    • cpu 缓存可能会导致一个线程修改了另一个线程无法及时感知
  • 保证可见性

    • volatile

      • 线程对共享变量的副本做了修改,会立刻刷新最新值到主内存中
      • 线程对共享变量的副本做了修改,其他其他线程中对这个变量拷贝的副本会时效,其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载
    • synchronized

      • 当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中
      • 当线程获取锁时,JMM 会把该线程对应的本地内存置为无效,从而使得被监听器保护的临界区代码必须从主内存中读取共享变量,从而实现共享变量的可见性
    • Lock

      • 其原理同 synchronized
    • 原子类

      • 每次操作拿到的都是主内存中的最新值,每次 set 的值也会立即写到主内存中

(3)有序性

  • 有序性是指程序执行的顺序按照代码的先后顺序执行

    • 编译器优化乱序(即时编译技术):编译器在不改变单线程程序语义的前提,根据 CPU 的指令流水线生成的执行指令重排序
    • CPU 指令乱序执行:现代处理器采用了指令级并行技术,将多条指令重叠执行。如果不存在数据依赖性,则处理器可以改变语句对机器指令的执行顺序
    • StoreBuffer 缓冲导致数据存入乱序:由于处理器使用读写缓冲区,使得加载和存储操作看上去可能是在乱序执行
  • 保证有序性

    • happens-before原则

      • happens-before 原则是 Java 内存模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,也就是说发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到
    • synchronized

      • synchronized 可以保证同一时间只有一个线程访问代码块,而单线程环境下,JMM 能够保证代码的串行语义,虽然使用 synchronized 的代码块,还可以发生指令重排序,但是 synchronized 可以保证只有一个线程执行,所以最后的结果还是正确的
    • volatile

      • volatile的底层是使用内存屏障来保障有序性的
      • 写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后
      • 读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前

4、happens-before 原则

  • 程序顺序规则(Program Order Rule):一个线程中的每个操作,happens-before 于该线程中的任意后续操作
  • 监视器锁规则(Monitor Lock Rule):对一个锁的解锁,happens-before 于随后对这个锁的加锁
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读
  • Thread.start() 规则(Thread Start Rule):如果线程 A 执行线程 B.start()(启动线程B),那么线程 A 的 B.start() 操作 happens-before 于线程 B 中的任意操作
  • Thread.start.join() 规则(Thread Join Rule):如果线程 A 执行线程 B.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 B.join() 操作成功返回
  • 程序中断规则(Thread Interruption Rule):对线程 interrupt() 的调用 happens-before 于被中断线程的interrupted() 或者 isInterrupted()
  • finalizer规则(Finalizer Rule):一个对象构造函数的结束 happens-before 于该对象 finalizer() 的开始
  • 传递性规则(Transitivity):如果 A happens-before B,且 B happens-before C ,那么 A happens-before C

5、内存屏障

  • 内存屏障(Memory Barrier),也称为内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以执行此点之后的操作,语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果
  • JMM 层面的内存屏障(编译器屏障)
屏障类型指令示例说明
LoadLoad BarrierLoad1
LoadLoad
Load2
确保Load1数据的装载先于Load2及所有后续装载指令的装载
StoreStore BarrierStore1
StoreStore
Store2
确保Store1数据对其它处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
LoadStore BarrierLoad1
LoadStore
Store2
确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存
StoreLoad BarrierStore1
StoreLoad
Load2
确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载,StoreLoad Barrier 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令