一、内存模型
1.1、含义
内存模型描述的是“对于一个变量A,线程t1能够读取到线程t2对该变量的写入”的条件,即关于内存可见性(这里和后续如无特别说明,都指代“广义内存可见性”)的条件。比如“线程t1对一个volatile变量的写入,线程t2能否读取到”,“线程t1将一个final变量传递给线程t2,在t2中该变量对应的实例是否真的已经构造完成,而不存在由于重排序导致的构造未完成情形”。
有4点需要注意:
- 线程t1和t2可以是同一个线程
- 在有些场景中,不是“线程”,而是“协程”
- 有不同层级的内存模型,比如“处理器层级的内存模型”,“语言层级的内存模型”
- “内存模型”主要围绕
可见性问题(“可见性问题”和“有序性问题”等价)
,这是主要矛盾,其中所涉及到的“原子性问题”是次要矛盾,因此,JMM内容归属于“可见性与有序性”分类
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规则
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关键词》