理解 Java 内存模型

本贴最后更新于 1978 天前,其中的信息可能已经东海扬尘

Java 内存模型

Java 内存模型(JMM)的抽象示意图如下图所示:
imagepng

从抽象的角度来看,JMM 定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程已读/写共享变量的副本。

这里需要注意的是,本地内存是 JMM 的一个抽象概念,并不是真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件与编译器优化。

那么,JMM 怎么来控制线程之间共享变量的同步呢?首先我们需要了解下,同步共享变量在 JMM 中主要有什么挑战:重排序。

重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分 3 种类型。

  1. 编译器优化的重排序。编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序,以充分利用寄存器等特性。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术,将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能在乱序执行。

从源代码到最终执行的指令序列,会分别经历以上 3 种重排序。上述的 1 属于编译器重排序,2 跟 3 属于处理器重排序。这些重排序可能会导致多线程出现内存可见性问题。对于处理器重排序,JMM 的处理器排序规则会要求 Java 编译器在生成指令序列时,插入特定的内存屏障(Memory Barriers/Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

数据依赖性

如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖性分为三种:

  1. 写后读:写一个变量之后,再读这个变量
  2. 写后写:写一个变量之后,再写这个变量
  3. 读后写:读一个变量之后,再写这个变量

上面 3 种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变数据依赖关系的两个操作的执行顺序。

这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

内存可见性

当某个线程正在使用对象而另一个线程同时正在修改该对象,需要确保当一个线程修改了对象状态后,其他线程能够立刻看到发生的变化。

根据 JMM 的内存模型,线程之间的交互发生在主内存中,但对变量的修改又发生在自己的本地内存中,不做控制的话,会造成读写共享变量的错误。

内存可见性问题是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。

happens-before

从 JDK5 开始,Java 使用新的 JSR-133 内存模型。JSR-133 使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内的,也可以是不同线程之间的。

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个 unLock 操作先行发生于后面对同一个锁额 lock 操作
volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C
线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始

内存屏障

内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier 即读屏障和写屏障。

Load Barrier: 在指令前插入 Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
Store Barrier: 在指令后插入 Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

java 的内存屏障通常所谓的四种即 LoadLoad,StoreStore,LoadStore,StoreLoad,实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
LoadLoad 屏障:对于这样的语句 Load1; LoadLoad; Load2,在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。
StoreStore 屏障:对于这样的语句 Store1; StoreStore; Store2,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。
LoadStore 屏障:对于这样的语句 Load1; LoadStore; Store2,在 Store2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。
StoreLoad 屏障:对于这样的语句 Store1; StoreLoad; Load2,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile 语义

volatile 的内存屏障策略非常严格:

  1. 在每个 volatile 写操作前插入 StoreStore 屏障;
  2. 在每个 volatile 写操作后插入 StoreLoad 屏障;
  3. 在每个 volatile 读操作前插入 LoadLoad 屏障;
  4. 在每个 volatile 读操作后插入 LoadStore 屏障;

由于内存屏障的作用,避免了 volatile 变量和其它指令重排序、线程之间实现了通信,使得 volatile 表现出了锁的特性。

final 语义中的内存屏障

对于 final 域,编译器和 CPU 会遵循两个排序规则:

  1. 新建对象过程中,构造体中对 final 域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;
  2. 初次读包含 final 域的对象引用和读取这个 final 域,这两个操作不能重排序;(意思就是先赋值引用,再调用 final 值)

换句话说:必需保证一个对象的所有 final 域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:

  1. 写 final 域:在编译器写 final 域完毕,构造体结束之前,会插入一个 StoreStore 屏障,保证前面的对 final 写入对其他线程/CPU 可见,并阻止重排序。
  2. 读 final 域:在上述规则 2 中,两步操作不能重排序的机理就是在读 final 域前插入了 LoadLoad 屏障。

X86 处理器中,由于 CPU 不会对写-写操作进行重排序,所以 StoreStore 屏障会被省略;而 X86 也不会对逻辑上有先后依赖关系的操作进行重排序,所以 LoadLoad 也会变省略。

参考资料

《Java 并发编程的艺术》
https://www.jianshu.com/p/2ab5e3d7e510
http://ifeve.com/linux-memory-barriers/
https://www.kernel.org/doc/Documentation/memory-barriers.txt
https://juejin.im/post/5b0b56f6f265da0dd6488083
https://blog.csdn.net/jiuqiyuliang/article/details/62216574
https://jcp.org/en/jsr/detail?id=133
https://www.cnblogs.com/dolphin0520/p/3920373.html

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1090 引用 • 3467 回帖 • 297 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3165 引用 • 8206 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...