Java Memory Model

本文将从JMM的理论模型和系统设计角度切入讲述并发工具的内存语义与实现细节。

JMM存在的目的

Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台下都能达到一致的内存访问效果。

JMM

JVM内存模型操作

主内存操作

  • lock:将一个变量表示为一条线程独占的状态。
  • unlock: 将一个处于锁定状态的变量释放,释放后的变量才可以被其他线程锁定。
  • read: 将一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • write: 将store操作从工作内存中得到的变量值放入主内存的变量中。

工作内存操作

  • load: 把read操作从主内存得到的变量值放入到工作内存的变量副本中。
  • use: 把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到需要使用变量复制的字节码指令时执行这个操作。
  • assign: 把一个执行引擎的接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store: 把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

内存操作执行基本规则

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。

  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。

  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

JVM内存模型特性

  • 原子性 JVM对基本数据类型的访问读写(上述操作)是具备原子性的。
  • 可见性 当一个线程修改了共享变量的值,其他线程能够立刻知道这个修改。而volatile变量较普通变量能够保证多线程场景下线程在每次读写前都能刷新。
  • 有序性 本线程内,操作都是有序;多线程场景下,线程间操作是无序的。

Happen-Before先行发生法则

先行发生是JMM中定义的两项操作之前的偏序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能够被B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。

具体体现:

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

指令重排

编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

单线程重排序:

  • 数据依赖性:程序的任意两个操作的执行是可能具有一定的依赖性,不能改变。
  • as-if-serial语义:单线程程序的执行结果不能改变。
  • 程序顺序规则: happens-before的顺序规则不能修改。

多线程重排序:

  • 顺序一致性模型:概念上模型只有一个单一的全局内存,所有操作线程在每一步操作后看到的内存内容都是一致的。实际上并不能完全保证,只能保证同步程序在进出临界区内代码各个线程的内存视图能够一致。

内存屏障

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

java内存屏障

java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

Java同步工具的内存语义及实现

锁的内存语义及实现

锁的语义决定了临界区代码的执行具有原子性。

内存语义

锁的释放可以让线程向获取同一个锁的线程发送消息。
锁的获取可以让线程对应的内存失效使得临界代码必须从主内存获取共享变量。

实现细节

公平锁获取通过AbstractQueuedSynchronizer即AQS实现,通过一个整型的volatile变量state来维护同步状态。拿锁时,tryAcquire方法会查看state值是否为0,即无锁状态,并将state值设置为传入变量acquires,如果state不为0,且owner不是current线程,则返回false.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected final boolean tryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
if(c == 0) {
if( isFirst(current)) && compareAndSetState(0, acquires) {
setExclusiveOwnerThread(current);
return true;
}
}
else if(current == getExclusiveOwnerThread()){
int nextc = c + acquires;
if(nextc < 0)
throw new Error("Max lock count exceeded");
setState(nextc);
return true;
}
return false;
}

非公平锁的获取不需要tryAcquire方法中通过isFirst(current))方法进行竞争,而是直接调用compareAndSetState(int expect, int update)。

(非)公平锁释放通过tryRelease(int releases)实现:

1
2
3
4
5
6
7
8
9
10
11
12
protected final boolean tryRelease(int releases){
int c = getState() - releases;
if( Thread.currentThread() != getExclusiveOwnerThread())
throw new IlleagalMonitorStateException();
boolean free = false;
if( c == 0){
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

CAS 内存语义及操作内容

内存语义

CAS更新操作,同时具有volatile读和volatile写的内存语义。

操作内容

CAS是处理器的一种操作,是native方法API。

  1. 确保对内存读-改-写的原子性。
  2. 禁止CAS指令前后读写指令重排。
  3. 把缓存区的所有数据刷新到内存中。

volatile 内存语义及实现

内存语义

volatile写与锁的释放有相同的内存语义,volatile读与锁的获取有相同内存语义。

实现细节

  • 通过插入内存屏障,来组织编译器/操作系统进行指令重排序。

  • 通过关联读/写操作和使用操作(用之前必须从主内存读,assign后必须写入主内存,以及写happens-before读规则)强制CPU的缓存失效来保证内存可见性。
    volatile的内存屏障策略如下:

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
    volatile-write

  • 在每个volatile读操作后分别插入LoadLoad屏障,和LoadStore屏障;
    volatile-read

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。

volatile强制缓存失效策略如下:

  • 线程的Load、read和Use进行关联:只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。

  • Assign和所有线程的store,write进行关联只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)。

  • 不同变量的上述的两段操作顺序一致假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。

    Happens before法则: 前一个操作的执行结果要对第二个操作可见。

final 内存语义与实现细节

final关键字可以放在static域,实例成员域,和局部变量三种变量前。其中final修饰的局部变量的可以作为线程的局部变量传递给子线程。也能保证并发情况下的内存语义。

内存语义

对于final域,编译器和CPU会遵循两个重排序规则:

  • 新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;(废话嘛)
  • 初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(晦涩,意思就是先赋值引用,再调用final值)

总之上面规则的意思可以这样理解,必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:

实现细节

  • 写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止this指针赋值与final域写被重排序(this = new Object(){ finalField = …})。(如果没有,普通域的写可以被重排到构造函数外)
    final-write

  • 写final域的成员域:构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
    final-element-write

  • 读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障,这个阻止了读取this引用和读取final域的重排序(isntance.finalField)。
    final-read

X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。

只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指 lock 和 volatile 的使用),就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值。

this逸出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
    i = 1;                              //1写final域
    obj = this;                          //2 this引用在此“逸出”
}
public static void writer() {
    new FinalReferenceEscapeExample ();
}
public static void reader {
    if (obj != null) {                    //3
        int temp = obj.i;                //4
    }
}
}

Concurrent包的内存语义及实现

Concurrent包底层实现依赖如下图所示:

Cocurrent

延迟初始化问题讨论

延迟初始化是在需要实例的时候再进行初始化,从而达到提升程序初始化性能的目的。然而延迟初始化需要考虑多线程并发访问,和指令重排序问题。

静态域延迟初始化

静态域的延迟初始化能通过final关键词实现,因为final静态域能保证多线程安全初始化,同事也能保证computeFieldValue()方法不会溢出FieldHolder的构造方法。

1
2
3
4
5
6
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField(){
return FieldHolder.field;
}

成员域延迟初始化

单重检查模式

单重检查模式能够确保大多数情况的fiel的同步,但是当computeFieldValue()执行和field赋值可以重排序,导致在第一次检查时其他线程可能看到不完整的field值,并返回。

1
2
3
4
5
6
7
8
9
private volatile FieldType field;

private FieldType getField(){
FieldType result = field;
if(result == null){
field = result = computeFieldValue();
return result;
}
}

双重检查模式

双重检查模式通过synchronized和volatile的内存语义,3, 4对其他线程可见,且其他线程在1处的读不会重排序到2语块的内部,能够确保在线程更新field值时,与其他线程查看field值之间的读写能够顺序执行。

局部变量result的使用能够保证尽量少次数的访问field和取锁,提升运行效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private volatile FieldType field;

private FieldType getField(){
FieldType result = field;
if(result == null){
result = field;
if(result == null){ // 1
synchornized(this){ // 2
field = result = computeFieldValue(); //3, 4
}
}
}
return result;
}