volatile与CPU Lock指令前缀

volatile保证线程之间的可见性和指令重排序稳定

​ volatile主要就是保证线程之间对单一变量的有序性和防止指令的重排序,本篇博文在于深入到操作系统的角度来解释什么是volatile关键字以及其如何实现对一个变量的代码有序性。

引入

引入1 单例模式下的volatile 要保证指令执行有序

​ 首先先看一段volatile关键字经常被面试中问到的单例模式的代码

/**
 * @author XX
 * @date 2020/10/9 - 9:20
 */
public class Singleton {
    private volatile Singleton instance;
    private Singleton(){}
    public Singleton getInstance(){
        if(instance == null){
            synchronized (Singlton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在这个代码中有关双重加锁检测不是重点暂时不表,而在于成员变量的volatile的成员变量是否是一定需要的为什么要给这个变量加上volatile关键字。

那么就应该要说明如果不加上volatile关键字在这个单例模式中可能出现的问题,在语句 instance = new Singleton()这个语句是非原子性的,这条语句会被编译为多条字节码:

/**
 * @author XX
 * @date 2020/10/9 - 9:31
 */
public class V1 {
    public static void main(String[] args) {
        Object obj = new Object();
    }
}

这一个简简单单的new Object操作可以被编译为:

    LINENUMBER 9 L0
    NEW java/lang/Object    // 对应下方第一条
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V//对应下方第二条
    ASTORE 1//对应下方第三条

这五条指令,重点说其中三条指令来点出一个对象被创建的流程:1、根据对象类型(这里就是Object类型)的大小来在Java堆中开辟对应大小的内存。2、给这一段已经分配的内存附上初始值,java中默认为null,数值则为0,String为空串,boolean为false(扯远了)。3、将已经初始化完成的内存地址值链接上对象。

JVM会将指令重排来使得执行效率最高(为什么重排序能提升效率后面会有Tips来解释),并且要知道两条指令为什么能发生重排序肯定是要两条无关指令才能重排序,那么在上面的new对象的过程中可能是1->2->3执行,但是2和3操作之间没有必然联系关系,那么就可能先将对象的地址值先赋值给了对象(这里我理解的是对象的本质是指针),然后再在这块空间上赋值上初始值。

如果2、3指令之间发生了重排序的话那么就有可能发生一种特别特殊的情况,回到上面的getInstance方法:

public Singleton getInstance(){
        if(instance == null){
            synchronized (instance){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

在JVM进行了1 、 3操作后此时有个新线程调用了getInstance方法此时他判断instance != null了那么就直接返回了instance但是此时的instance是还未被赋值的数据,JVM还没有对其附上初始值,其拿到这个未执行初始化方法的对象就可能发生多线程并发错误!

引入2 内存可见性的含义 保证数据多线程的可见性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jTzMDxiA-1602212608063)(X:\笔记\截图\内存图.png)]

CPU硬件上的缓存是如图所示的,为什么要引入这张图,是因为在CPU级别缓存的速度是L1>L2>L3>内存的,CPU希望能减少和内存之间的IO过程那么将内存中的数据会先缓存到CPU中的L级缓存中,等该段程序执行完成后再写回到内存中,但是运行过程中是可能导致CPU中的缓存不一致的问题,即有别的线程更改了内存中某个值,但是另一个线程在另一个CPU中无法得知,那么就需要有一种机制能实现各个缓存之间的同步问题。

写者的话

这里引入这张图的本意是想说MESI协议的缓存一致性原理,但是要描述清楚MESI协议和volatile本质并不是触发MESI协议来完成内存同步一致性而是通过CPU上的LOCK指令之后再写。

Volatile防止指令重排序和线程可见性原理

​ 在JSR中有规定防止指令重排序的四个等级:SS/SL/LS/LL 其都是内存屏障的意思,S对应写,L对应读,通俗一点解释便是SS为在屏障之前的所有写操作写完了,屏障之后的才能写其他的同理。这四种等级的屏障知识Java标准规范中定义的,那么实际实现的方式是由JVM的提供商自己决定的,下面所描述的实现方案都是基于Hotspot的实现方式。

​ volatile变量在进行写操作的时候就会触发自己的S屏障,这个屏障的实现方式是使用CPU的LOCK前缀指令

1.Lock前缀指令会引起处理器缓存会写到内存
当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中
2.一个处理器的缓存回写到内存会导致其他处理器的缓存失效
处理器使用嗅探技术保证内部缓存 系统内存和其他处理器的缓存的数据在总线上保持一致。

综合上面两条实现原则,我们了解到:如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

在Hotspot中,一个变量成员如果加上了volatile关键字那么在进行赋值操作的时候会先判断下是否是volatile的,如果是volatile关键字的那么会用操作系统下的LOCK指令,其功能就是将当前CPU的操作都"锁"起来,这个锁就是我们平时说的总线锁

总线锁,处理器使用**LOCK#**信号达到锁定总线,来解决原子性问题,当一个处理器往总线上输出LOCK#信号时,其它处理器的请求将被阻塞,此时该处理器此时独占共享内存。

加上这个锁后此时无论你对该变量是读/写更改操作都得等到你写完了之后才能让别人进行读写。通过上面的描述可以知道HotSpot通过一个Lock前缀指令就达到了线程之间可见性和代码有序性两种性质的完成,但是给CPU加锁这个锁的粒度过粗!

​ 这个锁导致了该CPU中所有的变量都是独享的,其总线被加锁后,其上所有信号都会被阻塞知道这个锁被释放。但是Hotspot为什么如此实现呢?可能是因为懒吧,没有实现粒度更细的锁或者用其他操作系统API实现,但是这种鲁莽的做法却是最有效的,也是最可靠的实现volatile关键字的做法。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值