并发编程|彻底搞懂volatile

什么情况下会使用volatile关键字呢?

在多线程开发过程中,操作同一个共享变量,想让每个线程对这个共享变量的修改对其他线程立即可见,这个时候就需要使用volatile关键字进行修饰

为什么在多线程下,对同一个共享变量的修改,不会对其它线程立即可见呢?

解释这个问题的话我们得说下内存模型的结构了,内存模型结构其实分为共享主内存和线程私有内存,在线程启动时候首先会从主内从中将变量读取到当前线程的私有内存中,后续的修改操作都在自己的私有内存中进行,在私有内存中修改的数据不会立即同步到主内存中,必须要等带处理器将这部分数据刷新到主内存中,但是这个时间是不确定的,所以在这期间,对变量的修改是不会对其他线程可见的。在这里插入图片描述

为什么volatile能够让线程对变量的修改对其他线程可见呢

线程可见性造成的原因我们在上面已经讲过了,所以要解决这个问题的话,我门肯定得让线程对变量的修改之后立即刷新到主内存中,当然这还不够,还得通知其它线程将当前缓存的这个变量失效,重新从主内存中读取,当然了这个不是由线程自己去通知啦。那么他是如何通知其他线程重新从主内存中读取数据呢,我们看下他们是如何借助关键字volatile来完成线程间的通信: 1. 线程A对volatile变量进行了修改 2. JMM把修改的共享变量刷新到主内存中 3. 线程B读取vloatile变量的时候,JMM会把本地内存的数值置为无效 4. 线程B则直接从主内存中重新读取最新的数值

在这里插入图片描述

为什么说volatile变量的写和读 内存语义 等同于锁的释放和获取内存语义?

因为我们知道对volatile变量的写完之后,jmm会把共享变量刷新到内存中,其实我们在释放锁的时候,JMM也是做了这个操作,因为jmm需要保证这个线程在获取锁之后在本地内存中修改的数据对其他线程可见,所以在释放锁之后,必须立即将共享变量的数据刷新到内存中。 volatile的读 会使共享变量的内存地址失效,重新读取主内存中的数据,为了看到上一个线程锁释放后刷到主内存的数据,获取锁的时候也会执行同样的操作,从主内存中读取最新的共享变量的数据。        所以总结来说:volatile变量的写和读 内存语义 等同于锁的释放和获取内存语义

好多人都说volatile具有禁止指令重排的特性,那它到底是个啥?

在执行程序时,为了提高性能,编译器和处理器通常会对我们的代码进行重新排序。
编译器重排序会在不改变单线程语义的情况下,进行重排序,例如如下这段代码,1和2 是会被进行重排序的,

a = 1  // 1
if(flag) {  
a = 1 // 2
}

但是这些如果是在多线程情况,指令重排序会造成与期望结果不一致的问题。
为了禁止这些重排序的动作,JMM会在指令中插入内存屏障,禁止处理器和编译器对这些代码重排序。
所以禁止指令重排是为了解决那些在多线程情况下,一些不可预知的问题的。
volatile刚好就具备了这种特性。

借助volatile禁止指令重排特性 来优化双重检查锁在单例中的隐患

单例的实现方式有两种:一种是懒汉模式,另外一种是饿汉模式,饿汉模式是线程安全的,懒汉模式是线程不安全的的,它是通过双重检查锁来避免线程安全带来的问题,但是他真的安全么?
public final class VirtualCore {
private static VirtualCore instance = null;
    private VirtualCore() {
    }
    public static VirtualCore get() {
        if(instance == null){
        synchronized(VirtualCore.class){
        if(instance == null){
        instance = new VirtualCore();
        }
        }
        }
        return instance;
    }
}

看代码中,已经做了两次非空判断,应该是已经安全了,但是不是我们认为的这样的,他还是不安全的,因为我们在 new VirtualCore(),这个动作的时候,有可能会发生指令重排,因为new VirtualCore();这个指令其实是分为三个步骤

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

如果2和3的顺序在指令重排后,顺序颠倒,则有可能产生这种情况,线程A 执行了new VirtualCore();而这时候线程B会直接使用还未初始化的实例去使用,结果会造成空指针。
解决方案:可以使用volatile 修饰instance,明确禁止处理器对其指令重排,达到线程安全的目的

总结:本篇博文通过几个疑问,让大家对volatile理解的更加深刻,后续会更新更多的有关并发编程相关的教程,感谢关注。大家可以关注我的公众号"乐哉开讲",领取更多资料。

在这里插入图片描述

微信搜一搜【乐哉开讲】关注帅气的我,回复【干货】,将会有大量面试资料和架构师必看书籍等你挑选,包括java基础、java并发、微服务、中间件等更多资料等你来取哦。
书读的越多而不加思考,你就会觉得你知道得很多;而当你读书而思考得越多的时候,你就会越清楚地看到,你知道得很少。——伏尔泰

猜你喜欢

转载自blog.csdn.net/weixin_34311210/article/details/108305847