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

一、简介

volatile能保证可见性、有序性。

synchronize与volatile的区别就是,synchronize能保证原子性,而volatile不能。

特殊情况下可以保证原子性(比如long,64位,先读前32位再读后32位,如果这个long变量用volatile修饰就能保证原子性)。

它靠内存屏障和禁止重排序来实现可见性、有序性。

二、可见性

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

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

2.可见性(synchronize、final、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和volatile可以保证有序性)

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

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

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

JSR-133中定义了如下的 happens-before 规则:

  1. 单一线程原则:在一个线程内,程序前面的操作先于后面的操作。
  2. 监视器锁规则:一个unlock操作先于后面对同一个锁的lock操作发生。
  3. volatile变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,也就是说读取的值肯定是最新的。
  4. 线程启动规则:Thread对象的start()方法调用先行发生于此线程的每一个动作。
  5. 线程加入规则:Thread 对象的结束先行发生于 join() 方法返回。
  6. 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
  8. 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。

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

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

JMM四类内存屏障

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

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

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

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

  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;
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值