Java Memory Model 笔记

Jul 2, 2016


What is a Memory Model, and Why would I Want One?

考虑这条语句 avariable = 3,在很多种情况下线程读不到这个结果。可能是编译器重排了指令、变量被放到寄存器里,处理器并行执行指令等。

Platform Memory Models

处理器提供不等等级的缓存一致性。 确保每个处理器知道其他处理器做了什么是非常昂贵的,很多时候这种信息时不必要的。所以处理器放松了内存一致性来提升性能。 为了让屏蔽掉开发者对不同架构的内存模型的不同的感知,Java 提供了了自己的内存模型,其他的交给 JVM 处理。

一个构想模型 (mental model) 就是假定所有操作顺序执行,每个读都能读到最后一次写的内容。这种模型被叫做sequencial consistency。开发者总是认为是这种模型,然而并没有多处理器是这种模型。

Reordering

JMM 允许每个线程看到不同的动作执行,使得在缺乏同步的情况下弄清顺序更加困难。同步禁止了编译器等对指令的重排。

The Java Memory Model in 500 Words or Less

JMM 定义了一个偏序 (partial ordering) 在程序里的全部动作上,叫happen before。为了确保动作 B 能看到 A 的结果,在 A 和 B 之间就需要一个 happens-before关系。

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

Piggybacking on Synchronization

这章讲的好像是利用自身的语句顺序来确保 happens-before,没怎么看懂。

Publication

Unsafe Publication

当 publish object 时我们要保证 happens-before。

很简单的一个例子

public class Test {
    private staticResource resource;
    
    public static Resource getInstance() {
        if (resource == null)
            resource = new Resource();
        return resource;
    }
}

假设两个线程,A 先执行,那么 A 和 B 之间并不存在 happens-before 关系,所以在 B 看来,resource 可能被部分初始化了。

Safe Publication

happens-before的 promise 比 safe publication 更强力。如果 X 在 A 和 B 之间 safe publication,只保证 X 状态可见,其他的 A 变量并没有保证。
如果 A put B happens-before fetches B, 那么 B sees the everything A did before.

Safe Initialization Idioms

Safe Initialization 的方法有很多种。

  1. 利用synchronized来确保
  2. 利用 JVM 初始化先于类的使用,放在 static class 或者变量里初始化。

Double-checked Locking

这里说了一种「臭名昭著」的模式 - Double-checked Locking

看看下面代码:

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
       public static Singleton getInstance() {

       if(instance == null) {
           synchronized(Singleton.class) {
              if(instance == null) {
                  instance = new Singleton();
              }
           }
       }
       return instance;
    }
}

乍一看很叼吧。。双重保险,只让一个进程去初始化变量。然而还是会有问题,获得引用地址和对象初始化并没有什么关系,可能一个进程在初始化的时候另一个进程就是用了不完全初始化的变量了。

Initialization Safety

final修饰的变量禁止了和初始化变量相关的指令的重排序,而且所有读写操作都被冻结了,直到初始化完成。