1.volatile内存语义的实现
被volatile修饰的变量除了是线程可见的(写操作到工作内存中之间刷新到主内存,读操作时把工作内存清空,之间读取主内存到工作内存),volatile还可以限制编译器重排序和处理器重排序。下表是JMM针对编译器制定的volatile重排序规则表。

从上表可以可以简单总结一下规律
只要第二个操作是volatile写,第一个操作是什么都不能与之重排序
只要第一个操作是volatile读,第二个操作是什么都不能与之重排序
第一个操作是volatile写,第二个操作是volatile读,两个操作不能进行重排序
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列插入内存屏障来禁止特定类型的处理器重排序
内存屏障的类型,如下表

JMM为volatile插入内存屏障的策略:

volatile写插入内存屏障的示意图:

由上图可以看出StoreStore屏障可以确保在volatile写操作之前,普通的写操作已经对任意处理器可见了。因为StoreStore屏障可以保证上面的普通写操作已经在volatile写之前刷新到主内存中。
volatile读插入内存屏障的示意图:

由上图可以看出,在volatile读后面插入LoadLoad屏障,LoadStore屏障确保后面的普通读写操作与上面的volatile读不会发生重排序。
上述的volatile的内存语义都是JDK1.5后的加强版,之前没有限制volatile对普通读写的重排序限制。在JDK1.5之后,严格限制了volatile的写/读要和锁的释放和获取具有相同的内存语义。
2.锁的内存语义
锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
2.1锁释放获取的happens-before关系
下面是锁释放和获取的代码

假设线程A执行writer方法,线程B随后执行reader方法,则这个过程包含的happens-before关系可以分为3类:
根据程序次序规则:1先于2,2先于3,4先于5,5先于6
根据监视器锁规则:3先于4
根据传递性规则:2先于5
转化成示意图如下:

如上图所示,由于2先行5,所以线程A中的共享变量在A释放锁后,线程B在获得同一把锁后立马可见。
2.2锁获取和释放的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。以上面的程序为例,A线程释放后,共享数据的状态如下图:

当线程获取锁时,JMM会把该线程对象的本地内存设为无效。从而使锁保护的临界区代码必须从主内存中读取共享变量。下图是示意图

这里我们可以看出锁释放和volatile的写具有相同的内存语义,锁的获取和volatile的读具有相同的内存语义。
2.3 CAS操作的内存语义
通过分析CAS的源码(这部分分析详细见《Java并发编程的艺术》)发现CAS的内存语义同时具有volatile的读和写语义。因为CAS调用了本地的方法,本地方法在解析成汇编码的时候,发现指令前多加了一个lock前缀。根据Intel的手册对lock前缀的描述:
利用总线锁定(老处理器,性能低)和缓存锁定(现代处理器,性能相当较高)来确保对内存的读-改-写操作是原子执行的。
禁止该指令,与之前和之后的读和写指令重排序
把写缓冲区的所有数据刷新到内存中
根据2,3点我们就可以看出CAS操作同时具有volatile读写的两个内存语义。
通过对ReentrantLock的源码分析(这部分分析详细见《Java并发编程的艺术》),公平锁和非公平锁的实现如下:

现在我们有两种方式来实现锁的释放或获取的内存语义了。
利用volatile变量的写-读所具有的内存语义
利用CAS所附带的volatile的读和写内存语义
由于CAS具有volatile的读和写内存语义,现在Java多线程通信的方式有下面四种

如果我们看了concurrent包的源码,我们会发现一个通用化的实现模式:
首先,将共享变量声明为volatile
然后,使用CAS的原子条件更新来实现线程之间的同步
同时,配合以volatile的读写和CAS具有的内存语义来实现线程之间的通信。
3.final的内存语义
对于final变量的重排序,编译器和处理器要遵循以下两个规则:

也就是说final变量的赋值不会被重排序到构造器以外,和在使用对象引用的时候,final变量肯定是初始化过的(这个初始化不是指默认值的初始化,是手动的初始化)。
通过例子来说明这两个原则

第一种情况假设线程B获取对象引用和读普通成员变量没有发送重排序,下面是这种情况的序列图:

构造方法中普通变量逸出了构造方法,所以可能会引起脏读。
第二种情况是,线程B中的引用对象赋值操作,和读取普通变量的操作可能发生重排序。

这种情况是线程B读取普通变量的操作被重排序,线程A还没有对普通变量初始化,线程B就开始读,因此造成的脏读。
所以说普通变量和final变量的区别就在于,final变量可以保证线程在使用包含这个final变量的对象引用时,final变量已经初始化过了。但是这个保证也是有前提的,那就是对象的引用不能从构造函数中“逸出”,我们看下面的情况:

这个程序可能会发生下面的情况:

所以在日常编程时一定要注意,不要让对象引用在构造方法中逸出,很危险。




