Skip to the content.

首页

Java内存模型(JMM)


Java Memory Model

规范

happens-before

如果操作A happen before 操作B,那么操作A在内存上所做的操作对操作B都是可见的,不管它们在不在一个线程。

这并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行,只要保证重排序之后的执行结果与按happens-before关系来执行的结果一致即可。


主内存和工作内存

JMM定义的用于主内存和工作内存之间同步的原子性变量操作

Java内存模型只要求read和load操作、store和write操作顺序执行,而没有保证必须是连续执行。

解决多线程问题


volatile

作用

可以将对volatile变量的读取操作视作加上了同步的get和set方法,只是没有显式加锁操作。

指令重排序

是减少CPU中断的一种技术,其可以保证结果与顺序执行的结果一致,但是没有义务保证与并行执行的结果一致。

内存屏障

保证屏障之前和之后的代码不能交换执行顺序。写屏障会让写入缓存中的最新数据写入主内存,读屏障会使得高速缓冲区的数据失效强制重新从主内存中加载数据。

既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

MESI协议是应用最广泛的多核CPU缓存一致性协议,但CPU并没有严格遵守,因为会降低执行效率;而volatile是Java语言的保证,用来防止指令重排,无论是单核还是多核都需要。

public class Demo {
    private static boolean running = true;
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (running) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> running = false);
        t1.start();
        Thread.sleep(1000);
        t2.start();
        t1.join();
        t2.join();
    }
}

此代码运行结果是t1线程会进入死循环无法退出,解决方案是使用volatile修饰running;但并不是因为可见性的原因导致,而是由于JIT优化,t2线程修改running值前,t1线程已经运行了1秒中,t1中的代码会被判断热点代码,进而直接将running替换为true,导致代码进入死循环;如果关闭掉JIT优化或者注释掉sleep代码,由于CPU缓存一致性协议,最终t1仍会读取到最新的running值并退出循环。

单例模式的双重检查锁为什么需要volatile?

private volatile static Singleton instance = null;
 
public static Singleton getInstance() {
    if (instance == null) { // 保证可见性
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

初始化操作分为三步(1)new:创建对象实例,分配内存空间、(2)invokespecial:调用构造器方法,初始化对象、(3)aload_0:存入局部方法变量表,synchronized并不能防止指令重排,故如果(2)(3)发生了重排,则可能出现当其他线程判断instance非空时,instance指向的对象还未调用构造器方法。


安全发布

不正确的发布

public Holder holder;

public void init() {
    holder = new Holder(42);
}

public class Holder {
    private int n;

    pulic Holder(int n) { 
        this.n = n; 
    }

    public void assert() {
        if (n != n) {
            throw new AssertError();
        }
    }
}

多线程执行assert方法时可能抛出异常,因为域n不是final的,在Object的构造函数中会先将默认值0写入n中。

安全发布对象的方式