Java内存模型-volatile内存语义和实现原理

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。在实际的并发编程过程中,Java的内存模型和内存可见性是保证并发安全的最基本理论,今天先通过对轻量级锁volatile的实现原理,逐步引入Java内存模型的各种概念和实现机制。

以一个简单的单例模式设计过程引入今天的主题

1、使用灵活性低的synchronized实现单例

    /**
     * 静态方法上使用synchronized同步标识,线程安全,
     * 但是每次调用该方法都进行加锁处理,频繁的线程切换会导致效率很低
     * @return
     */
    public static synchronized LazySingletonSycn getInstance() {
        if(instance == null) {
            instance = new LazySingletonSycn();
        }
        return instance;
    }

2、基于DCL(double check lock)机制实现,但是非线程安全。因为new对象的时候非原子性操作(有3个操作,解释如下)导致其他线程可能获得一个未初始化的对象

   /**
     * 两次null是否空的判断,貌似完美的设计,
     * 但是因为存在指令重排,可能线程1在new对象的时候,线程2直接返回一个没有初始化的对象
     * @return
     */
    public static LazySingletonSycn getInstance3DCL() {
        if(instance == null){
            synchronized (LazySingletonSycn.class){
                if(instance == null) {
                    /**
                     * 问题根源在此:
                     * new对象可拆分成3个指令执行
                     * 1、memory = allocate() //分配内存
                     * 2、ctorInstance(memory) //初始化对象
                     * 3、instance = memory //instance指向刚分配的内存地址
                     * 其中2和3可能会指令重排,如果发生指令重排,先执行了指令3,此时线程2执行第一个if判断时就是false
                     * 此时直接返回一个没有初始化的instance对象,导致系统错误
                     */
                    instance = new LazySingletonSycn();
                }
            }
        }
        return instance;
    }

3、主角登场!使用volatile和DCL实现线程安全的单例,volatile能够使指令重排不超越内存屏障

    /**
     *  volatile在读写时会插入内存屏障,这样会禁止编译器和cpu对指令重排
     */
    private static volatile LazySingletonThreadSafeVolatile instance;

    public static LazySingletonThreadSafeVolatile getInstance() {
        if(instance == null){
            synchronized (LazySingletonThreadSafeVolatile.class) {
                if(instance == null){
                    instance = new LazySingletonThreadSafeVolatile();
                }
            }
        }
        return instance;
    }

volatile内存语义
1、可见性。对一个volatile变量的读,总是能看到其他线程对这个变量最后的写入。
2、原子性。对任意单个volatile变量的读/写具有原子性,但是类似volatile++这样的操作是不具有原子性的。

实现原理
volatile是如何保证可见性的呢?
对volatile修饰的变量进行写操作时,JIT会多生成一个汇编代码:
lock add $0x0,(%esp) 注明:此代码我没有实际查看过,参照于《并发编程的艺术》
Lock前缀的指令在多核处理器下会引发两件事情:
1)将当前处理器的缓存行的数据写回到主存
2)这个写回主存的操作会是其他处理器中缓存该内存地址的数据无效

显然这个Lock指令是CPU级别的,而且涉及到CPU缓存一致性原则。
先介绍一个术语:缓存行,指的是CPU中高速缓存的最小存取单位

注明:这里面涉及的CPU高速缓存模型,具体细节会在单独的文章中专门介绍,本篇侧重介绍Lock指令在CPU缓存行中的处理机制,这个机制是实现volatile缓存一致性的关键。

基本实现过程:
当对volatile修饰的变量进行写操作,JVM会向处理器发送一条Lock前缀的指令,将CPU所在的缓存行数据写回到内存中,为了保证其他处理器的缓存和当前缓存一致,每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行读写操作时会重新从系统内存中读取到缓存行中。

大致流程如下:

Java内存模型-volatile内存语义和实现原理_第1张图片
LOCK指令流程.png

关于LOCK指令的流程有两个地方要说明,
1)Lock前缀指令引发处理器缓存回写到内存,执行该指令期间,会声言处理器的LOCK#信号,早期的多核环境下LOCK#信号会锁住总线,但是开销大。近期的多核处理器下,会锁住内存区域缓存(各个处理器共享的缓存),也就是“缓存锁定”。缓存一致性会保证写操作的原子性,同时阻止两个以上的处理器同时修改此缓存。
2)处理器使用MESI(M=Modified修改,E=Exclusive独占,S=Shared共享,I=Invalid无效)协议去维护内部缓存和其他处理器缓存的一致性。

MESI的具体含义如下
Modified:本CPU写,则直接写到Cache,不产生总线事务;其它CPU写,则不涉及本CPU的Cache,其它CPU读,则本CPU需要把Cache line中的数据提供给它,而不是让它去读内存。
Exclusive:只有本CPU有该内存的Cache,而且和内存一致。 本CPU的写操作会导致转到Modified状态。
Shared:多个CPU都对该内存有Cache,而且内容一致。任何一个CPU写自己的这个Cache都必须通知其它的CPU。
Invalid:一旦Cache line进入这个状态,CPU读数据就必须发出总线事务,从内存读。

总结:对一个volatile变量的读,总是能看到其他线程对这个变量最后的写入,也就是说Java内存模型确保所有线程看到这个变量的值都是一致的。
实现原理:JVM向处理器发送一个LOCK前缀的指令,这个指令能够让当前处理器将缓存回写到内存,其他处理器通过嗅探总线传播的数据将当前处理器的缓存行失效,后续的写操作时需要从内存中重新读取到缓存行。

你可能感兴趣的:(Java内存模型-volatile内存语义和实现原理)