0.什么是Java内存模型:这篇文章讨论什么不讨论什么
内存模型规定了在给定程序的条件下,某个特定的程序执行过程是否合法。内存模型只是Java运行环境与上层Java程序员之间关于内存操作语义的约定,并不规定Java内存管理的具体实现。这篇文章也只是试图用易于理解的方式描述这种约定,而不会讨论内存模型约束下内存管理机制的具体实现。
1. 定义几个基本概念
1.1 共享变量:
存储在堆内存中可以被跨线程访问的变量,包括:实例属性、类属性和数组元素。
1.2 跨线程操作
下面罗列Java内存模型所定义的所有跨线程操作:
- 读非volatile共享变量
- 写非volatile共享变量
- Synchronization actions, which are:
(下面几项都被称作同步操作)
- 读volatile共享变量
- 写volatile共享变量
- 加锁Monitor
- 解锁Monitor
- 某一线程的第一个和最后一个跨线程操作
- 启动线程的操作和检测线程是否终止( T.isAlive() or T.join())的操作
线程模型只关心跨线程操作,线程内操作的对象(局部变量)与跨线程操作的对象没有交集,所以可以毫无影响的被排除在讨论范围之外。
2. 夹逼Java内存模型
2.1 编程顺序
每个线程在执行过程中,除非遇到跨线程操作,程序的语义由单线程程序执行语义决定。一旦遇到跨线程操作,跨线程操作的语义由内存模型决定,内存模型与所谓的跨线程语义等价。具体的讲即:如果线程在执行过程中遇到一个跨线程读操作,该读操作的语义由内存模型决定,并且此后线程执行过程中所读取的值都和该跨线程读操作所读到的值相等。
基于上述澄清,编程顺序可以定义为:某一线程在执行过程中,可能会发起一系列的跨线程操作。这些跨线程操作,在受到线程内语义的约束下的实际执行顺序,被称作编程顺序。
2.2 串行一致性模型
串行一致性模型是一种很强的一致性模型,定义模型最为简单,但是没有把内存模型所有的优化潜力挖掘出来。
串行一致性模型要求所有的跨线程操作都与编程顺序保持一致,准确的讲即:对于任意一个跨线程读操作,必须看到所有满足下列要求的,针对同一变量的跨线程写操作:
- 在编程顺序中,该写操作在该读操作之前
- 在编程顺序中,不存在任意的针对同一变量的写操作出现在该读操作之前,该写操作之后
2.3 同步顺序
1.2提出的6个同步操作引出了同步顺序:
- 针对同一Monitor的解锁操作在同步顺序中先于其后所有的加锁操作
- 针对同一volatile变量的写操作在同步顺序中先于其后所有的读操作
- 启动某一线程的操作在同步顺序中先于该线程中第一个跨线程操作
- 线程类的初始化在同步顺序中先于该线程的启动操作
- 某一线程中的最后一个跨线程操作在同步顺序中先于检测该线程是否终止( T.isAlive() or T.join())的操作
- 对某个线程的interrupt操作在同步顺序中先于对该线程是否interrupt检测(Thread.interrupted or Thread.isInterrupted or catch InterruptedException)的操作
2.4 Happens-before顺序
若有两个跨线程操作a1和a2,按照Happens-before顺序a1在a2之前,则记作:a1 -hb-> a2。Happens-before顺序由下面四条规则定义:
- 同一个线程内,如果有两个跨线程操作x和y,按照编程顺序,x在y之前执行,那么x -hb-> y。
- 如果有两个跨线程操作x和y,按照同步顺序的规则,x先于y,那么x -hb-> y。
- 某个对象构造器的结束 -hb-> 该对象finalizer的开始
- Happens-before顺序满足传递性,如果满足x -hb-> y,并且y -hb-> z,那么必有:x -hb-> z。
Happens-before关系是多个程序操作之间的一种特殊关系,若 x -hb-> y,并不意味着,物理的x一定在y之前执行。准确而言,x -hb-> y只保证在逻辑上,对于任意的两个观察操作o1和o2,当满足o1 -hb-> o2时,o1和o2观察到的执行顺序是x在y之前执行。
2.5 Happens-before一致性模型
Happens-before一致性模型是一种较弱的一致性模型,对一致性的要求放的较宽,只满足Happens-before一致性模型的内存管理实现可以更对的挖掘内存模型的性能优化潜力。但是只满足Happens-before一致性模型会出现上层Java程序员难以接受的结果。
在Happens-before一致性模型中,对于任意一个跨线程读操作,应该可以看到满足下列条件的,针对同一变量的所有跨线程写操作w:
- r -hb-> w 不成立
- 不存在针对同一变量的另一个跨线程写操作w'满足:w -hb-> w',并且w' -hb-> r。
2.6 Causality
只满足Happens-before约束的内存模型是不够强的,会产生令上层Java程序员无法接受的结果。产生这样无法接受的行为的来源是Happens-before约束没有关于数据依赖性的约束条件,在Java内存模型中被称作Causality。
下面这个例子是用来说明Causality的经典例子:
初始条件x == y == 0;
线程A |
线程B |
r1 = x; |
r2 = y; |
if (r1 != 0) y = 1; |
if (r2 != 0) x = 1; |
由于Happens-before约束没有任何数据依赖性也就是Causality的约束,上述例子在编译器或者是处理器重排序优化的条件下,可能优化成下述执行序列:
- x = 1;
- r1 = x;
- y = 1;
- r2 = y;
执行结果为r1 == r2 == 1;这一结果是不符合逻辑的,上层Java程序员无法接受。
3. Java内存模型
3.1 容易理解的描述
所以综上所述,我们不难总结完整的Java内存模型的完整内容应该为:比串行一致性松,比Happens-before紧的一致性约束。具体的就是在Happens-before一致性约束的基础上再加上Causality约束,也即,Optimizer在对跨线程操作进行reorder优化的时候要与数据依赖性约束保持一致,不能破坏原有编程顺序的数据依赖性。
在上面的经典例子中,同一线程的三个操作:读,if判断,写,之间都存在着数据依赖性,所以在一个线程内的跨线程操作不能进行任何reorder优化。
3.2 严格的描述
下面两个文献包含严格的形式化描述
- JSR 133 Java Memory Model and Thread Specification, Chapter 7
- The Java Language Specification, Java SE 7 Edition, 17.4.6 ~ 17.4.9
3.3 补充规定
上面提出的是Java内存模型的主题规则,出此之外还有一些补充规则,用以实现对一些有用的feature和特别的平台的环境的支持。
3.3.1 final字段的语义
final字段的基本语义是该字段只能被赋值一次,一旦赋值以后无法修改,但是Java支持使用反射的方式对final字段的修改。
在Java内存模型方面,提供对final字段的freeze语义支持,具体为:
- Java内存模型保证final字段在构造器执行退出时freeze,无论构造器是否是成功的执行完成退出。即:按照Happens-before顺序,如果对final字段的读取之后进行,则读取到final的赋值,否则为默认值。
- Java内存模型规定,如果一个构造器调用了另一个构造器,那么,freeze在被调用的构造器退出时完成。
- 如果对final字段使用反射进行了修改,那么freeze在修改完成之后立即完成
值得注意的是,由于final字段的取值在首次赋值之后保持不变,所以,Java可能针对final字段进行以下优化:
- 如果final字段用了常量表达式进行赋值,那么编译器可能在出现该final字段的地方直接使用常量表达式进行替换,那么以后通过反射方式对final字段进行的修改可能不会被观察到
- 对final字段进行的修改和读取之间不被判定为具有数据依赖性,不受Causality约束保护。
3.3.2 写保护字段
写保护字段是历史产物,System.in, System.out和System.err是static final的字段,但是可以通过System.setIn, System.setOut和System.setErr来进行修改,被区分的称作写保护字段。
写保护字段在实际处理中被当作是普通字段,但是只能被System类中的代码修改。
3.3.3 非volatile double、long和64位引用字段的操作原子性支持
在对非volatile的double、long和64位引用字段进行写时,写操作可能被拆分成两次32位的写操作。Java内存模型保证对非volatile double、long和64位引用字段的读取和写入具有原子性,永远不会读到两个不一致的32位。
3.3.4 Word Tearing
针对特殊的平台环境,略。