作者:Jeremy Manson and Brian Goetz 原文:www.cs.umd.edu/~pugh/java/…
jdk1.5 出了一份新的java内存模型和线程规范 jsr-133, 制定规范的各路大神们把他们的商议的过程,结果完整的保留在了下面这个网站里 www.cs.umd.edu/~pugh/java/…
在网站开头列了一份的常见问题解答资料

Q1. 什么是 memory model[0]?
在多处理器系统中,处理器通常具有一层或多层内存高速缓存[1],通过本地缓存可以加快对数据的访问(因为数据更接近处理器)和减少共享内存总线[2]上的流量来提高性能(本地缓存读写数据,减少对内存的操作)。内存缓存可以极大地提高性能[3]。但缓存带来了许多新的挑战。例如,两个处理器同时检查相同的内存地址时会发生什么?在什么条件下能看到相同的值?(总而言之, 数据一致性问题)
关于硬件部分, 可以关注一下冬瓜哥的 《大话计算机》 和 《大话内存模型》。
[0] 此处的内存模型指的是'硬件内存模型'并非'java内存模型'(JMM)。
[1] 现代处理器通常有三层高速缓存(有些两层), L1 L2 L3. L1 L2是处理器独享缓存, L3是处理器间共享缓存
[2] 内存总线: 计算机各种功能部件(CPU,内存,硬盘)之间传送信息的公共通信干线
[3] 访问内存通常比访问缓存多几个数量级的机器周期。
在处理器级别,内存模型定义了当前处理器数据写入内存对其他处理器可见的充要条件[4]。处理器可以分为强一致性内存模型[5]和弱一致性内存模型[6]。 强一致模型中处理器看到的值始终是正确的。弱一致性模型需要特殊指令(内存屏障[7])来刷新本地处理器高速缓存中的缓存行[8]。这些内存屏障通常在lock和unlock时执行; 对于高级语言的程序员来说,它们是不可见的。
[4] MESI: 内存一致性协议,保证内存和CPU高速缓存的数据一致性,还有很多类似的协议, 但MESI
是网上资料最多且硬件系统架构中中普遍在用的。MESI可以保证强一致性内存模型的数据一致性
https://blog.csdn.net/ZoeyyeoZ/article/details/51804647?locationNum=13
[8] 缓存行是高速缓存的最小单位,默认b4byte。
把缓存行比作一包薯片, 我们要读取的变量a是其中一片。
想像对话如下: '老板,两片乐事,正方形不加糖'
在多处理器系统架构下强一致性模型需要的内存屏障更少,编码难度低,但性能不佳;弱一致性模型性能高,多处理系统的可扩展性更佳,支持更大的内存,但需要大量的内存屏障。
影响数据一致性的另一大问题,编译器代码优化重排[9]。例如:编译器认为将某个写操作推迟一些执行更好;只要不影响程序的语义,就能自由的执行。如果编译器推迟了写操作,另一个线程就可能看到旧的值。
也可以将写操作提前, 其他线程将在操作实际发生之前看到写入的值。只要符合内存模型, 准许指令 在运行时,编译时,硬件层面的重排。
[9] JAVA编译器(JIT即时编译器将class编译成汇编)优化重排.
上代码:
Class Reordering {
int x = 0, y = 0;
public void writer() { // Thread1
x = 1;
y = 2;
}
public void reader() { //Thread2
int r1 = y;
int r2 = x;
}
}
两个方法分别在不同线程同时进行, 如果调用reader() r1=2, 我们会认为r2肯定等于1; 但此时编译器也许会指令重排序, 将y的写入插入到x之前; 结果 r1=2, r2=0.
Java内存模型(JMM)描述了哪些多线程行为是合法的, 线程间如何通过内存进行交互(线程通信)。它描述了程序中变量间的关系以及真正的[10]计算机系统中内存/寄存器读写的底层细节。在各种硬件和编译器优化[11]的情况下确保代码能被正确执行。
Java中有几个关键字: volatile, final, and synchronized. 可以帮助程序员向编译器描述程序的并发需求,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。
[10] 操作系统, 计算机硬件
[11] 硬件优化 弱一致性模型 多处理 缓存以及一些无法想象的细节优化
Q2.其他语言像C++,有内存模型吗?
大多数其他编程语言,如C和c++,在设计时没有直接支持多线程。这些语言对于编译器和CPU结构体系[12]重排序行为的保护机制,严重依赖于程序中所使用的线程库(例如pthreads)、编译器、以及代码运行的平台所提供的保障。
[12] https://www.cnblogs.com/wangyiwei/p/7831282.html
Q3.JSR 133 是什么?
1997年以来, 发现Java Language Specification 第17章[13]定义的JMM中存在一些严重缺陷.这些缺陷会导致一些令人困惑的行为(例如final字段在改变值时能够被看到)以及破坏编译器执行常见优化的能力。
[13] https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html
JMM是一项雄心勃勃的工作;这是编程语言规范第一次尝试合并内存模型,为各种体系结构的并发性提供一致的语义。不幸的是,定义一个既一致又直观的JMM比预期的要困难得多。JSR 133为Java语言定义了一个新的JMM,它修复了早期JMM的缺陷。为此,需要更改final和volatile的语义。
完整的语义[14]可以在 www.cs.umd.edu/users/pugh/…, 但是形式语义[15]不会这么细碎。它是令人惊讶发人深省的,就像同步关键字一样简约但不简单。我们无需了解正式语义背后的细节,JSR-133目标是创建一组形式语义,为final,volatile,synchronized提供直观的框架。
jsr-133目标包括以下几点:
1.保留已经存在的安全保证(像类型安全)以及强化其他的安全保证。例如,变量值不能凭空创建:线程观察到的每个变量的值必须是被(其他或当前)线程合理设置[16]的。
2.正确的同步语义尽可能简单直观
3.不完全或不正确同步程序的语义,以便将潜在的安全风险降到最低。(提供反面教材)
4.程序员能够通过jsr-133自信的推断出多线程程序执行结果
5.可以在各种流行硬件系统体系上涉及正确的高性能JVM实现.(这是真的牛)
6.新的初始化安全保证。如果一个对象被正确的构造(正在构造的对象不允许引用"逃逸") ,对象构造函数中设置的final字段被所有线程见到时都是最新的值。
7.对现有的代码改动最小(金科铁律)
[14] 完整的语义。 例如 volatile:volatile是什么, 为什么需要volatile, 怎么实现的,
在什么情况下使用以及测试用例。
[15] 形式语义= java语言规范 + JVM规范 https://docs.oracle.com/javase/specs/index.html是"宪法",
规定了怎么做。 而在http://www.cs.umd.edu/users/pugh/java/memoryModel中可以找到前应后果。
[16] Java编程语言内存模型的工作原理是检查同步相关的每个读取,并根据某些规则检查该读取所观察到的写入是否有效.
[17] this引用逃逸: 在对象构造期间, 其他对象或者线程能够调用构造中对象。(spring boot 有一步 freeze bean);
https://stackoverflow.com/questions/1588420/how-does-this-escape-the-constructor-in-java;
如何安全的构造对象防止this引用逃逸->https://www.ibm.com/developerworks/java/library/j-jtp0618/index.html
Q4.重排序意味着什么[18]?
执行程序时为了提高性能,提高并行度,编译器和处理器常常会对指令做重排序, 导致程序对变量(对象实例, 数组元素, class static字段)的访问可能和程序指定的顺序不同。
重排序分三种类型:
- 编译器优化的重排序。
- 指令级并行的重排序(处理器重排)。现代处理器采用了指令集并行技术(Instruction-Level Parallelism,ILP)重叠执行多条指令。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序(处理器重排)。数据在寄存器,处理器高速缓存,主存中移动时与程序指定的顺序不同
[20]。 例如,代码如下
void writeToVar(){
a = 1;
b = 2;
}
b 不依赖于 a,编译器重排序时, b可以先a一步刷新到主存。有许多潜在重排序来源, 例如JIT,CPU高速缓存。
[18] 原文是What is meant by reordering? 不明白what is 和 what is meant by有什么区别
[20] 在硬件通信中有很多的异步场景, 有异步的地方就有数据不一致。
编译器、运行时和硬件遵守as-if-serial语义,在单线程中保证指令重排序不对程序顺序产生影响。多线程中不能保证程序按指定的顺序运行
大多时候,一个线程并不关心其他线程在做什, 当它开始关心时,就需要同步了。
Q5.旧的内存模型有什么问题?
旧内存模型有几个严重的问题, 比如在JVM中会发生一些本不该允许的指令重排序。
- 某些场景下能看到final字段的默认值。
- volatile字段 读和写的指令重排序
- ....
JSR-133 在As-If-Serial语义的基础上增加了happen-before原则禁止指令重排序
Q6.错误同步意味着什么?
假设有两条线程t1 t2 共享变量
1.t1写a
2.t2读a
3.写入和读取没有通过同步[21]排序
当以上情况发生时, 即发生了数据竞争[22], 发生了数据竞争的程序被称为错误的同步
[21] 要理解一个程序是否正确同步,有两个关键的想法:
'访问冲突': 如果至少有一个访问是写访问,那么对共享变量或数组元素的两次访问(读|写)就会发生冲突。
'happen-before原则'
[21] 当程序包含冲突访问并没有使用happen-before原则排序, 就被称为数据竞争
Q7.同步[22]做了什么?
同步有几个方面.
- 互斥
- 刷新缓存使所有线程可见
- 禁止指令重排序
最容易理解的是互斥('mutual exclusion') —— 一次只有一个线程能持有监视器, 在监视器上同步意味着一个线程进入一个受监视器保护的同步块,在第一个线程退出同步块之前,任何其他线程都不能进入该监视器保护的块。
但同步不仅仅是互斥, 同步确保一个线程的操作对同步在相同监视器上的其他线程可见。退出同步块之前, 线程释放(release)[24]监视器, 刷新缓存到主内存,以便其他线程可以看到这个线程执行的写操作。在进入同步代码块之前我们获取(acquire)[24]监视器,它的作用是使本地处理器缓存失效, 从主存中加载最新变量。然后, 当前线程可以看到变量的最新值。
新的内存模型模型语义对内存操作(读,写,锁定,解锁)和其他线程操作(start and join)规定了操作顺序。当一个动作在另一个动作之前,禁止重排序,并且第一个动作一定对第二个动作可见。排序规则如下:
- 线程中每个操作都遵守程序的执行顺序
- 对监视器的解锁在加锁之前
- 对volatile的写操作对之后发生的读操作可见
- start()操作发生在已启动线程的任何操作之前
- 线程中的任何操作发生在其他线程对当前线程的join()之后
这意味着线程在同步块之内的所有内存操作对其他线程可见, 因为内存操作在释放监视器之前, 而释放操作在获取之前。
[22] 1.5之前 Java的同步方法只有同步关键字synchronized, 新的内存模型增加了volatile, happen-befor原则.
[23] 获取释放 是同步的两个重要步骤, 可将同步简单的分解成以下步骤:
获取监视器 获取失败等待 获取成功进入代码块 释放监视器 唤醒等待线程争抢锁
Q9.在新的JMM中final字段是如何工作的?
final字段可以在构造函数中set, 如果对象被安全的构造, 线程无需对final字段做同步就能够读取到最新值。
如何安全的构造一个对象:对象构造过程中禁止构造中的对象引用"逃逸(escape)"。(有关示例,参见 www.ibm.com/developerwo…
如果final字段是引用类型, 引用的地址是最新址, 但是里面的值需要额外加同步。
JMM尚未定义JNI(Java Native Interface)改变final字段的行为, 使用JNI改变final不知道会发生什么
Q10.voaltile有什么用?
线程间传递状态的关键词, 每次读取volatile都能看到其他线程的最后一次写入。volatile使高速缓存无效并禁止指令重排序。
旧的内存模型,对volatile字段不能指令重排序,但是可以重排序和volatile字段相关的普通字段。
新的内存模型对于在volatile周围的普通字段也有刷新缓存,禁止指令重排序的效果。使得写入volatile字段与释放监视器具有相同的效果,读取volatile字段与获得监视器具有相同效果。
示例如下:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
新内存模型: 执行reader时,如果v==true则一定能看到x==42 旧内存模型: 存在重排序, x不一定等于42
Q11.新JMM如何修复双重检查锁的问题?
方法一: volatile
public class Single{
private static volatile Something instance = null;
public Something getInstance() {
if (instance == null) {
synchronized (this) {
if (instance == null)
instance = new Something();
}
}
return instance;
}
}
方法二: 内部类
public class Single{
private static class LazySomethingHolder {
public static Something something = new Something();
}
public static Something getInstance() {
return LazySomethingHolder.something;
}
}
Q12.JVM实现入门教程
gee.cs.oswego.edu/dl/jmm/cook…
Q13.为什么我要在乎正确的同步?
- 并发性错误很难调试。
- 它们通常测试不出来,会在程序高负荷运行时出现,
- 难以重现和捕获
最好在程序运行前提前确保程序正确同步,虽然这并不容易,但也比出了问题再去调试容易得多