0%

JMM

一、内存模型

1.1、含义

内存模型描述的是“对于一个变量A,线程t1能够读取到线程t2对该变量的写入”的条件,即关于内存可见性(这里和后续如无特别说明,都指代“广义内存可见性”)的条件。比如“线程t1对一个volatile变量的写入,线程t2能否读取到”,“线程t1将一个final变量传递给线程t2,在t2中该变量对应的实例是否真的已经构造完成,而不存在由于重排序导致的构造未完成情形”。

有4点需要注意:

  • 线程t1和t2可以是同一个线程
  • 在有些场景中,不是“线程”,而是“协程”
  • 有不同层级的内存模型,比如“处理器层级的内存模型”,“语言层级的内存模型”
  • “内存模型”主要围绕可见性问题(“可见性问题”和“有序性问题”等价),这是主要矛盾,其中所涉及到的“原子性问题”是次要矛盾,因此,JMM内容归属于“可见性与有序性”分类

1.2、理论参考模型——顺序一致性内存模型

顺序一致性内存模型是一个理想化的理论参考模型,它提供极强的内存可见性保证。

顺序一致性内存模型有两大特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行
  2. (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。因为,在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见

1.3、实际内存模型

根据顺序一致性内存模型的描述可知,顺序一致性内存模型与“重排序(这里和后续如无特别说明,都指代‘广义重排序’)”的冲突是显而易见的。
比如:

  • 如果存在引入高速缓存-写缓冲器-失效队列机制导致的狭义可见性问题(属于“广义重排序”的一种),那么顺序一致性内存模型的第2点就不能满足
  • 如果存在狭义重排序问题,那么顺序一致性内存模型的第1点就不能满足

而我们知道,当不允许重排序时,执行性能会变得很差。

因此,总结来说,顺序一致性内存模型具有的优缺点分别如下:

  • 优点,易编程性好
  • 缺点,执行性能差

实际内存模型往往通过损失“易编程性”来获得“执行性能的提升”,具体手段就是允许重排序,所允许的重排序类型越多,则“损失的易编程性越大,执行性能提升越多”。

以下图1给出了几个实际内存模型(包括几个“处理器层级内存模型”和几个“语言层级内存模型”)在“执行性能-易编程性”二维图中的相对位置。

图1

1.3.1、处理器层级实际内存模型

处理器层级实际内存模型根据所允许的重排序类型分为4种:TSO,PSO,RMO和PowerPC。如表1所示。

表1

内存模型类别 Store-Load重排序 Store-Store重排序 Load-Load和Load-Store重排序 可以更早读取到其他处理器的写(也是一种重排序) 可以更早读取到当前处理器的写(也是一种重排序) 具体处理器案例
TSO Y Y SPARC-TSO,X86
PSO Y Y Y SPARC-PSO
RMO Y Y Y Y IA64[2]
PowerPC Y Y Y Y Y PowerPC[3]

备注:

  • SPARC架构见[1]
  • 两个PowerPC:一个是内存模型类别,一个是具体处理器案例

1.3.2、语言层级实际内存模型

比如有:

  • C++ 11的内存模型——C++ 11 MM
  • Java的内存模型——JMM
  • .Net的内存模型——CLR2.0 MM
  • Go的内存模型
  • Rust的内存模型

二、JMM

Java内存模型,英文全称为“Java Memory Model”,简称JMM。

在谈论JMM的时候,经常会涉及到JSR-133,这两者的关系是:JSR-133是一个定义JMM的规范,它增强改进了旧规范定义的JMM。

JMM有3大特性:

  • 单线程程序。单线程程序的执行具有顺序一致性,即“程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同”
  • 正确同步的多线程程序。正确同步的多线程程序的执行具有顺序一致性,即“程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同”
  • 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障,即“线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0、null、false)”

为便于程序员编写正确同步的多线程程序,JMM设计了一整套规范,作了基础设施实现(主要使用“内存屏障”消除不需要的“重排序”),向程序员提供交付了“JMM内存抽象模型”,“相关语法关键词”,“happens-before规则”以及其他相关语义确保。

2.1、JMM内存抽象模型

JMM向程序员提供一个抽象的内存模型,向下屏蔽千差万别的具体硬件差异,具体如图2所示。

关于这个抽象内存模型有以下几点说明:

  • Java进程的内存主要分为“程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,直接内存”,其中对于线程私有内存(程序计数器,Java虚拟机栈,本地方法栈)中的变量不会存在可见性问题,因此示意图中的主内存指代线程共享内存(Java堆,方法区,直接内存),且一般就是指“Java堆”
  • 在视图中每个线程都有一个私有的本地内存,本地内存中存储了主内存中共享变量的副本。但其实本地内存只是JMM用来便于描述的一个抽象概念,它并不真实存在,而只是“缓存、写缓冲区、寄存器”等机制的代指

图2

2.2、相关语法关键词

相关实现提供的语法关键词有:

  • synchronized
  • volatile
  • final

需要注意的是,上述关键词的完整语法语义不只来自于JMM,也来自于其他的Java语言语法规范。

2.3、happens-before规则

详见《happens-before规则》

2.4、其他相关语义确保

比如:

  • JMM不保证对long型和double型变量的简单读和写操作具有原子性;JMM保证对除了long型和double型之外的变量的简单读和写操作具有原子性
  • 在构造方法中对final实例成员变量A赋值,在构造方法返回之前,被构造对象的实例对象引用不被其他线程所见,在构造方法返回后,通过该实例对象引用读取A,那么对于该读操作来说,“构造方法中对A的赋值确保内存可见,如果A是个引用变量,其指向实例对象P,那么构造方法之前所有对P的操作都确保内存可见”。详见[10]中“final关键词内存可见语义规则”小节
  • 类的初始化(Class被加载之后,被线程使用之前)是线程安全的,具体是通过加锁实现的[9]

参考文献

[1]https://zh.wikipedia.org/wiki/SPARC
[2]https://baike.baidu.com/item/ia64
[3]https://zh.wikipedia.org/wiki/PowerPC
[4]https://tiancaiamao.gitbooks.io/go-internals/content/zh/10.1.html
[5]《Java并发编程的艺术》
[6]《JSR-133:Java内存TM模型与线程规范》
[7]https://developer.ibm.com/zh/articles/j-jtp03304/
[8]https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
[9]《Java并发编程的艺术》P72
[10]《synchronized-volatile-final关键词》

您的支持将鼓励我继续分享!