happens-before原则和内存屏障

happens-before原则和内存屏障

1、内存模型与指令重排

1.1、Java内存模型

每个线程都有自己的工作内存,线程的所有操作都必须在自己的工作内存中进行,而不能去操作主内存,并且不能去访问其它线程的工作内存,Java内存模型具有一些先天性的有序性,不需要通过其它的手段去保持自己的有序性,这个通常也被称之为happens-before原则,如果两个操作的执行次序无法从happens-before原则推导出来,那么就不能保证它们的有序性,虚拟机可以随意的对它们进行重排序。

1.2、指令重排序

概念:只要程序的最终结果与顺序化执行的结果一致,那么指令的顺序可以与代码的顺序不一致,此过程叫做指令的重排序。
意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

1.3、as-if-serial语义

含义:无论如何重排序,程序的最终执行结果是不能变的。
所以,在这个语义下,编译器为了保持这个语义,不会去对存在有数据依赖关系的操作进行重排序,如果数据之间不存在依赖关系,这些操作就会被操作系统进行最大的优化,进行重排序,使得代码的执行效率更高。

1.4、happens-before 原则

1.4.1、什么是happens-before 原则

含义:happens-before,字面意思,先行发生,以Java层面来理解的话就是,上一行代码的结果会被下一行代码所使用到,这就是先行发生,可以参考下面的代码。

// 比如下面这两行代码,第一行就必须在第二行之前执行
// 因为第二行用到了第一行的结果
int a = 1;
a = a + 1;

// 比如下面这两行代码,谁先谁后无所谓
// 因为底下的那行没有用到上面那行改变的值
a = 2;
int b = 5;

1.4.2、为什么要有happens-before 原则

Java并发编程必须要保证代码的原子性,有序性,可见性,如果只靠sychronized和volatile关键字来保证它,那么我们的代码写起来就显的相当的麻烦,从JDK 5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据。

1.4.3、happens-before 原则的八大子原则

1、程序顺序原则:在一个程序中的代码执行顺序是有序的,即使出现重排序,也会保证在单线程下的结果是一致的。
2、锁原则:在同一段程序中,要想对这段程序加锁,必须是前一段锁已经解锁了,才能对这段程序继续加锁。
3、volatile变量原则:如果一个线程先去写一个volatile变量,那么它必须先去读这一个变量,所以,在任何情况下volatile修饰的变量的值在修改时都是多其他线程是可见的。
4、线程启动原则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
5、线程终止原则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
6、线程中断原则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
7、传递性原则:假设A先于B,B先于C,那么A必须先于C。
8、终结原则:一个对象的初始化完成先于他的finalize方法调用。

2、内存屏障(了解即可)

内存屏障其实就是为了解决指令优化重排序带来的问题

2.1、Intel硬件提供的内存屏障

Intel硬件提供了一系列的内存屏障,主要有:
1、lfence,是一种Load Barrier 读屏障
2、sfence, 是一种Store Barrier 写屏障
3、mfence, 是一种全能型的屏障,具备ifence和sfence的能力
4、Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU 总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS,CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由 JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:

屏障类型 指令示例 说明
LoadLoad Load1; LoadLoad; Load2 保证load1的读取操作在load2及后续读取操作之前执行
StoreStore Store1; StoreStore; Store2 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore Load1; LoadStore; Store2 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad Store1; StoreLoad; Load2 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

2.2、volatile禁止指令重排序示例

public class DoubleCheckLock {
    private volatile static DoubleCheckLock instance;
    private DoubleCheckLock(){}
    public static DoubleCheckLock getInstance(){
        //第一次检测
        if (instance==null){
            //同步
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new  DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

比如上面这个单例的双重检测的代码,其实主要代码就是一个 new DoubleCheckLock(); 的操作,然后因为我们的new操作不是一个原子操作,所以就可能产生指令重排的现象,如下代码所示:

memory = allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null

因为第二行和第三行没有什么依赖关系,所以可以重排,例如如下面所示:

memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象

如果在执行完第二行时,发生了线程的上下文切换,然后其它线程刚好又引入了这个对象,是不是相当于就引用了一个空对象,因为这个时候我们这个对象刚好只是分配了内存空间却没有赋值,所以,如果要解决这个问题,那就给instance实例直接加上 volatile 属性,如下代码所示:

//禁止指令重排优化
private volatile static DoubleCheckLock instance;
上一篇:C#几种单例模式


下一篇:设计模式-JAVA-单例模式的八种实现方式