查看完整视频
小黑屋思过中,禁止观看!
评论并刷新后可见

您需要在视频最下面评论并刷新后,方可查看完整视频

积分观看

您支付积分,方可查看完整视频

{{user.role.value}}
付费视频

您支付费用,方可查看完整视频

¥{{user.role.value}}
课程视频
开始学习
会员专享

视频选集

Volatile的禁止指令重排

  • 课程笔记
  • 问答交流

上一级节课我重点讲解了volatile,谈到了的volatile的作用、Java内存模型、以及如何实现内存可见性等。

这节课我将重点讲解volatile的禁止指令重排,也就是并发编程三要素之一的:有序性。

为了助大家掌握好指令重排,本节课我会重点讲解以下5点:

1.有序性
2.指令重排
3.内存屏障
4.经典的单例的双重检测代码
5.Volatile禁止指令重排实现原理

什么是指令重排

指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。

指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。

需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。但是在多线程环境下,这么做却可能出现并发问题。

内存屏障(Memory Barrier)

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个:

1.是保证特定操作的执行顺序
2.是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。

Volatile经典的单例的双重检测代码

下面看一个非常典型的禁止重排优化的单例模式例子DCL(Double Check Lock双端检锁机制)

package com.yzxy.concurrent.basic;

public class Singleton {

    // 使用private将构造方法私有化,以防外界通过该构造方法创建多个实例
    private Singleton(){
        System.out.println(Thread.currentThread().getName() +" 私有构造调用了");
    }

    // 由于不能使用构造方法创建实例,所以需要在类的内部创建该类的唯一实例
    // 使用static修饰Singleton, 在外界可以通过类名调用该实例   类名.成员名

    private static Singleton instance;

    public     static Singleton getInstance(){
        //加锁前后都做一次判断

        //第一次检测
        if (instance==null){
            //同步
            synchronized (Singleton.class){

                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new Singleton();
                }
            }
        }


     return instance;

    }
    
}

运行这段代码我们可能会得到一个匪夷所思的结果:我们获得的单例对象是未初始化的。

为什么会出现这种情况?

交流专区
  1. 李鸿翼

    1.编译器为了优化性能,会进行指令重排

    2.happens-before原则就是前一个操作结果对后一个操作可见
    2.1程序顺序规则 在同一个线程中,前面的代码happens before 于后面的代码
    2.2volatile规则 对volatile变量进行的写操作 happens before 于后续这个对这个变量的读
    2.3传递性规则 A happens before B, B happens before C ,则A happens before C
    2.4锁规则:一个锁的解锁happens before后续对 这个锁的加锁
    2.5线程start规则:主线程A在启动子线程B后,子线程B能看到主线程在启动B之前的操作
    2.6线程join规则 :主线程A等到子线程B 执行完后,能看到子线程B的操作

    • mikechen

      一般谈到happens before会提到8大规则,特别是volatile规则(这个是面试的重点切入点)

  2. 路正银

    指令重排是为了进一步提高CPU的使用效率
    如果两条有依赖关系的指令挨的很近,后一条指令会因为等待前一条执行的结果,而在流水线中阻塞很久,占用流水线的资源,而指令重排试图拉开这两条指令之间的距离,当后一条指令进入CPU的时候,前一条指令的结果已经得到了,这样就可以不阻塞等待了。

    Happens-Before概念:在程序运营过程中,所有的变更会先在寄存器或者本地cache中完成,然后才会被拷贝到主存以跨越内存栅栏,此种跨越序列或顺序成为happens-before。
    happens-before本质是顺序,重点是跨越内存栅栏

    Happens-Before规则:
    1、程序次序规则
    2、管理锁定规则
    3、volitile变量规则
    4、线程启动规则
    5、线程终止规则
    6、线程中断规则
    7、对象终结规则
    8、传递性

    • mikechen

      是的,8条规则,回答正确 ✗咧嘴笑✗ 这里每个规则没有具体阐述,但是一般面试官会从1/2/3/8这4个规则来切入,至少这4个规则的适用场景是什么样的,具体可以在加深即可 ✗拳头✗

  3. JansenZhang

    1.在同一个线程中,书写在前面的操作happen-before后面的操作。
    2.同一个锁的unlock操作happen-before此锁的lock操作。
    3.对一个volatile变量的写操作happen-before对此变量的任意操作。
    4.如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
    5.同一个线程的start方法happen-before此线程的其它方法。
    6. 对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
    7. 线程中的所有操作都happen-before线程的终止检测。
    8. 一个对象的初始化完成先于他的finalize方法调用。

搜索