【十五】Java多线程之volatile(可见性、有序性、happens-before、内存屏障和禁止重排序)

版权声明:转载注明出处 https://blog.csdn.net/jy02268879/article/details/86063820

一、简介

volatile能保证可见性、有序性、但是不能保证原子性。它靠内存屏障和禁止重排序来实现可见性、有序性。

二、可见性

1.导致共享变量在线程间不可见的原因:

  1. 线程交叉执行
  2. 重排序结合线程交叉执行
  3. 共享变量更新后的值没有在线程工作内存和主存之间及时更新

2.可见性(synchronize、volatile可以保证可见性)

这篇只说volatile,synchronize在其他篇章聊。

可见性是说:一个线程对main memory的修改可以及时的被其他线程观察到。

被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

java内存模型

volatile修饰的共享变量保证可见性和有序性的原因: 

1.它将当前cpu cache  memory行的数据写回main memory

2.这个写回main memory的操作会使得其他CPU里缓存了该内存地址的数据无效

3.其他线程用到这个变量的时候会直接从main memory中读取,而不是使用cpu cache memory中的备份。

 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

三、有序性(synchronize、lock和volatile可以保证有序性)

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般是杂乱无序的。

在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,但会影响到多线程并发执行的正确性。

java内存模型的先天有序性happens-before原则

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

如果两个操作执行顺序无法从happens-before原则推导出来,那么他们就不能保证有序性,jvm可以随意的对他们进行重排序。

四、内存屏障和禁止重排序

JMM四类内存屏障

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2及所有后续装载指令
StoreStore Barriers Store1;StoreStore;Store2 确保Store1数据刷新到内存,先于Store2及所有后续存储指令
LoadStore Barriers Load1;LoadStore;Store2 确保Load1数据装载先Store2及所有后续存储指令
StoreLoad Barriers Store1;StoreLoad;Load2 确保Store1数据刷新到内存,先于Load2及所有后续装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载)完成之后,才执行该屏障之后的内存访问指令。

 JMM会针对编译器制定volatile重排序规则

是否重排序 第二步
第一步 普通读/写 volatile读 volatile写
普通读/写     NO
volatile读 NO NO NO
volatile写   NO NO

 编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

  1. 在每个volatile写操作的前面插入一个StoreStore屏障;
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障;
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障;
  4. 在每个volatile读操作的后面插入一个LoadStore屏障。

volatile写插入屏障示例:

 

volatile读插入屏障示例: 

五、常用场景

1.状态标记量

volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

因为context=loadContext()和inited=true之间的执行顺序不能保证(不符合happens-before中的任何一条),所以inited变量要用volatile修饰,以免出现这种情况:

线程1在context=loadContext()方法执行之前就先执行了inited=true,此时context根本没有加载

而,线程2恰好在此时判断出inited=true,就去执行doSomethingwithconfig(context), 此时context根本没有加载

2.双重检查

/**
 * 懒汉模式 -》 双重同步锁单例模式
 * 单例实例在第一次使用时进行创建
 */

public class SingletonExample4 {

    // 私有构造函数
    private SingletonExample4() {

    }

    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存

    // JVM和cpu优化,发生了指令重排

    // 1、memory = allocate() 分配对象的内存空间
    // 3、instance = memory 设置instance指向刚分配的内存
    // 2、ctorInstance() 初始化对象

    // 单例对象  volatile + 双重检测机制 -> 禁止指令重排
    private volatile static SingletonExample4 instance = null;

    // 静态的工厂方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample4.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }
}

猜你喜欢

转载自blog.csdn.net/jy02268879/article/details/86063820