一、Java语法语义
1.1、synchronized关键词
- 实现Java语言内置锁
- 实现happens-before规则——
对一个锁的解锁,happens-before于随后对这个锁的加锁
1.2、volatile关键词
- 针对所有基本类型和引用类型的简单读写操作(一个易混淆点,“变量自增”和“变量自减”不是简单读写操作)是否是原子的:
- 非volatile变量。除了
long
和double
之外的基本类型和引用类型,其非volatile变量的简单读写操作是原子的;而对于long
和double
基本类型,其非volatile变量的简单读写操作不一定是原子的[1] - volatile变量。对所有基本类型和引用类型,其volatile变量的简单读写操作是原子的
- 非volatile变量。除了
- 在现代多CPU/CPU核处理器架构下,为提升存取性能,引入了
高速缓存-写缓冲器-失效队列
机制,该机制会导致“狭义内存可见性”问题,使用volatile关键词解决该问题。“volatile变量写”对于后续的“volatile变量读”立即可见 - 实现happens-before规则——
对一个volatile域的写,happens-before于任意后续对这个volatile域的读
备注:
- 在上述3个语法语义中,最有价值的是第3个,其余两个价值不大
- 在上述第1点和第3点中,提及到了“后续”,其实在并发环境中,“后续”的判定十分困难。在应用第3点中的happens-before规则时,如何绕过“后续”的判定可参见《happens-before规则》
1.3、final关键词
- 表示“不可改变的”:
- final类不能被继承
- final方法不能被子类的方法覆盖,需要注意的是,final不能用于修饰构造方法
- final变量只能被赋值一次,赋值后值不再改变
- 提供一个内存可见语义规则,详见“3.3、final关键词内存可见语义规则”小节
二、实现概述
Java语言实现通过各种机制实现上述语法语义,手段包括使用“内存屏障”,“CAS操作”等。需要注意的是,这里所说的“内存屏障”和“CAS操作”是全层次的,即“既包含JVM指令层的内存屏障指令/CAS指令,也包含处理器指令层的内存屏障指令/CAS指令”,当然最后执行的是处理器指令层的内存屏障指令/CAS指令。
比如:
- 实现synchronized关键词相关的
hotspot/src/share/vm/runtime/objectMonitor.cpp:733
C++源代码处的OrderAccess::fence()
内存屏障指令调用 - 实现volatile关键词内存语义:
- volatile变量简单读写操作最终映射到的目标机器指令是原子的
- 为实现volatile变量写语义,对于volatile变量写操作应该有个绑定型内存屏障:其语义是“刷新缓存到内存,并使相应的缓存失效(这样子读取volatile变量时从内存重新加载最新值)”。需要绑定型内存屏障,是为了避免目标操作和内存屏障之间插入其他操作,关于绑定型内存屏障的介绍参见《内存屏障》
- 为实现volatile关键词的happens-before规则语义,在以上第2点的基础上,还需要按照以下方案添加内存屏障。可以发现,对于
在每个volatile写操作的前面插入一个LoadStore内存屏障
,在每个volatile写操作的前面插入一个StoreStore内存屏障
和volatile变量写操作应该有个绑定型内存屏障
的要求,可以用一个绑定型的StoreLoad内存屏障进行替代(因为StoreLoad内存屏障是“全能型”内存屏障,同时具有StoreStore,LoadLoad和LoadStore内存屏障的效果)。需要说明的是,这里的描述掺杂了笔者的自我理解,跟书本《Java并发编程的艺术》P43的描述并不一致,如有错误,请不吝赐教:- 在每个volatile写操作的前面插入一个LoadStore内存屏障
- 在每个volatile写操作的前面插入一个StoreStore内存屏障
- 在每个volatile读操作的后面插入一个LoadLoad内存屏障
- 在每个volatile读操作的后面插入一个LoadStore内存屏障
- 实现final关键词内存可见语义规则(在《Java Language Specification》中,通过
freeze action
来实现[2][3]):- 围绕对final实例成员变量赋值的构造方法C,C隐含有一个
return
语句,C内非final实例成员变量赋值操作允许重排序到return
之后,C内final实例成员变量赋值操作禁止重排序到return
之后,然后在紧邻return
之前插入一个特殊的StoreStore内存屏障,为什么说是“特殊的StoreStore内存屏障”呢?因为正常的StoreStore内存屏障如果在紧邻return
之前插入,那么C内所有的操作都应该被禁止重排序到return
之后,比如在“3.3、final关键词内存可见语义规则”小节中,示例代码3的构造方法内i
实例成员变量的赋值可重排序到构造方法之外。跟《Java并发编程的艺术》叙述有一定冲突,但感觉笔者的理解才是正确的 - 在“读实例对象引用”和“读该实例对象的final实例成员变量”之间插入一个LoadLoad内存屏障,避免上述两个操作的重排序
- 围绕对final实例成员变量赋值的构造方法C,C隐含有一个
具体实现有很多细节,太难也没有必要过度深入,因为主要矛盾在于宏观使用!
三、final关键词详解
3.1、编译期常量
编译期常量定义:如果一个类的成员变量(包括“类成员变量”和“实例成员变量”)由final关键词修饰,其类型为基本类型或者String类型,且在定义时赋“字面量”值,那么该变量为编译期常量。比如final int a = 10
,static final long b = 20L
,final String s = "hello world"
。
编译期常量的特殊之处:编译时会直接以对应的“字面量”值替换变量的使用。
3.2、匿名内部类访问外围变量
匿名内部类访问外围变量须遵守规则——匿名内部类来自外部闭包环境的自由变量必须是final的
。那么哪些变量是“自由变量”?只有匿名内部类所在外围方法的局部变量才是“自由变量”,也即所在外围类的实例成员变量和类成员变量都不是“自由变量”。
作出以上区分的本质原因在于访问变量的不同方式:
- 对于外围类的实例成员变量和类成员变量,匿名内部类通过“外围类类名”或者“外围类实例对象引用”的方式进行访问,后续对该变量的修改可传导到匿名内部类中
- 对于外围方法的局部变量,匿名内部类通过复制变量的方式进行访问,后续对该变量的修改不可传导到匿名内部类中,为避免“看似修改成功-实则修改未成功”的歧义和误导,语法索性直接规定该局部变量必须是
final
的,后续修改操作是非法的
需要注意的是:在JDK 1.8之前,对于自由变量需要显式声明final
关键词,而在JDK 1.8之后,编译器会帮你隐式声明,但我们最好还是显式声明下明确化这点。
1、例子1
原代码如下:
1 | public class AnonymousInnerClass0 { |
依次执行javac AnonymousInnerClass0.java
和javap -v AnonymousInnerClass0\$1.class
命令,可知匿名内部类的实际构造方法如下:
1 | class AnonymousInnerClass0$1 implements java.lang.Runnable |
由以上可知,匿名内部类访问外围类实例成员变量a
的方式是通过外围类实例对象引用this$0
,访问外围方法局部变量b
的方式是复制到本类的实例成员变量val$b
,故“// 1”处修改可传导到匿名内部类中,而“// 2”处修改不可传导到匿名内部类中,为避免歧义和误导,语法索性直接规定需要将b
变量设为final
的,使得“// 2”处操作直接为非法的。
2、例子2
原代码如下:
1 | public class AnonymousInnerClass1 { |
依次执行javac AnonymousInnerClass1.java
和javap -v AnonymousInnerClass1\$1.class
命令,可知匿名内部类的实际构造方法如下:
1 | final class AnonymousInnerClass1$1 implements java.lang.Runnable |
由以上可知,匿名内部类访问外围类类成员变量a
的方式是通过外围类类名,访问外围方法局部变量b
的方式是复制到本类的实例成员变量val$b
,故“// 1”处修改可传导到匿名内部类中,“// 2”处修改不可传导到匿名内部类中,为避免歧义和误导,语法索性直接规定需要将b
变量设为final
的,使得“// 2”处操作直接为非法的。
3.3、final关键词内存可见语义规则
3.3.1、含义
在《Java Language Specification》中[4],正式的英文叙述是:
The usage model for final fields is a simple one: Set the final fields for an object in that object’s constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object’s constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object’s final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are
形象的案例叙述是:
在示例代码0中,对于“// 13”这个final实例成员变量的读操作来说,“// 3 4 11”操作对其内存可见;而对于“// 12”这个非final实例成员变量的读操作来说,“// 10”操作可能重排序到构造方法之外,故不一定内存可见,更不用说“// 1 2”操作
示例代码0:
1 | public class FinalExample { |
中文正式叙述是:
对于final实例成员变量,当满足“在构造方法中对final实例成员变量赋值”和“在构造方法中不提前‘逸出’正在构造对象的引用”这两个条件时,就能够有语义确保:
使用构造完成对象访问其final实例成员变量,与该final实例成员变量相关的所有内存操作都可见
,为更好理解“与该final实例成员变量相关的所有内存操作都可见”,可参考示例代码1
在以上语义确保中,避免了本来需要考虑的两个问题:1)由于重排序,“构造方法语句”可能重排序到“读取到对象引用”之后,在以上场景中,“与该final实例成员变量相关的所有内存操作”不会重排序到“读取到对象引用”之后;2)由于重排序,“读取对象成员变量”可能重排序到“读取到对象引用”之前,在以上场景中,“读取对象final实例成员变量”不会重排序到“读取到对象引用”之前
示例代码1:
1 | public class FinalExample0 { |
要特别注意“final关键词内存可见语义规则”适用的限定条件:
- “在构造方法返回之前,被构造对象的实例对象引用不被其他线程所见”,故示例代码2中读线程B获取实例对象引用的方式不满足条件
- “在构造方法中对final实例成员变量赋值”,排除掉了
final类成员变量的两种赋值情形——“定义赋值”和“静态初始化语句赋值”
和final实例成员变量的另外两种赋值情形——“定义赋值”和“实例初始化语句赋值”
,在排除掉的情形中包括了编译期常量的情形,而编译期常量情形更是显而易见需要排除掉的
示例代码2
1 | public class ReferenceAhead { |
3.3.2、与happens-before规则的关系
“final关键词内存可见语义规则”与“happens-before规则”的关系:都属于“JMM提供的语义确保”范畴,两者相互独立,也没有语义确保强弱之分,只不过happens-before规则更易于理解和通用。
对于“示例代码0”,构造方法允许重排序,因此“// 1 2 3 4 5 6 7 8 9 10 11 12 13”操作之间本就存在可见性问题:
- 如果写线程A和读线程B是同一个线程,且
writer()
方法执行先于reader()
方法,那么根据happens-before规则——一个线程中的每个操作,happens-before于该线程中的任意后续操作
,就有“1 2 3 4 5 6 7 8 9 10 11 12 13”的顺序happens-before关系 - 如果obj变量增加
volatile
关键词修饰,且reader()
方法中if (obj != null)
语句执行结果为true,那么根据happens-before规则——一个线程中的每个操作,happens-before于该线程中的任意后续操作
、对一个volatile域的写,happens-before于任意后续对这个volatile域的读
和如果A happens-before B,且B happens-before C,那么A happens-before C
,就有“1 2 3 4 5 6 7 8 9 10 11 12 13”的顺序happens-before关系 - “// 13”操作满足“final关键词内存可见语义规则”,那么对于“// 13”操作所指代的final实例成员变量读操作来说,“// 3 4 11”操作对其内存可见
3.3.3、应用
1、案例1
来自《Java并发编程的艺术》P56。
示例代码3
1 | public class FinalExample { |
2、案例2
“示例代码4”展示了一种常见的使用Runnable匿名内部类的形式,一直有个困惑:在另外一个线程或者线程池中异步执行该Runnable对象的run()
方法时,与所传入外围变量相关的所有操作都内存可见吗?
特别需要注意的是,虽然都涉及到匿名内部类,但是这里跟“3.2、匿名内部类访问外围变量”小节的强调点不同,即“一个强调内存可见性,一个强调自由变量必须是final的”。
接下来进行释疑分析。
依次执行javac Outer.java
和javap -v Outer\$1.class
命令,可知Runnable匿名内部类的实际构造方法如示例代码5,如果进行合并,那么Outer类的最终等价形式如示例代码6。
在示例代码6中,“// 5”操作处隐含的“this.val$person”读操作满足“final关键词内存可见语义规则”,那么“// 1 2 3 4”操作对其内存可见。
示例代码4
1 | public class Outer { |
示例代码5
1 | class Outer$1 implements java.lang.Runnable |
示例代码6
1 | public class Outer { |
参考文献
[1]https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7
[2]https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5.1
[3]https://dzone.com/articles/final-keyword-and-jvm-memory-impact
[4]https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5
[5]https://blog.csdn.net/hzy38324/article/details/77986095
[6]https://stackoverflow.com/questions/27254152/what-exactly-does-the-final-keyword-guarantee-regarding-concurrency
[7]https://cloud.tencent.com/developer/article/1585263
[8]http://www.cs.umd.edu/~pugh/java/memoryModel/newFinal.pdf