一篇文章彻底搞懂volatile(深入理解)


前言

volatile是什么?
volatile解决什么问题?
volatile的实现原理?
带着问题出发,围绕并展开。


volatile是什么?

volatile是java提供的轻量级同步机制,volatile关键字有2个如下作用
1、保证volatile修饰的共享变量对所有线程是可见的,也就是相当于当一个线程修改了一个被volatile修饰的共享变量时,新值可以被其他线程立即感知到。
2、可以禁止指令重排、处理器优化问题。

1、volatile的可见性

可见性案例

线程A改变initFlag属性之后,线程B马上感知到。

public class VolatileVisibilitySample {
    volatile boolean initFlag = false;
    public void save(){
        this.initFlag = true;
        String threadname = Thread.currentThread().getName();
        System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }
    public void load(){
        String threadname = Thread.currentThread().getName();
        while (!initFlag){
            //线程在此处空跑,等待initFlag状态改变
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变");
    }
    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.save();
        },"threadA");
        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}

2、volatile无法保证原子性,为什么?

一个简单的多线程例子(无法保证原子)

public class VolatileVisibility {
    public static volatile int i =0;
    public static void increase(){
        i++;
    }
}

在并发的情况下,i变量的任何改变都会立刻反应到其他线程,但是在多线程的情况下,都调用increase()方法,就会出现线程安全问题。
因为i++操作本身并不是原子操作。
i++操作的实际顺序是分为读(通过指针或者常量池获取到值)、改(二个值的计算)、写(最后进行赋值)

i++可以分为如下三步
int x = i;//多线程的情况,线程1在这里读到了值,然后让出cpu资源,线程2拿到资源也读到了x=i的情况就会出现原子性问题
x = i+1;
i = x;

我们知道,因为我们所写的代码,最终是化为一条条指令在CPU上执行,而CPU执行的时间片,又是看快速轮动交替执行的,这样就有可能。
假设造成A线程刚读到值、或者刚改变了新值,还没来得及进行最后的赋值操作,时间片被分配到了B线程,B线程这时候拿到的公共区域的数据,还是A线程没有修改之前的值,然后进行i++,然后执行完了I++操作后,A线程又拿到了线程,执行完了最后的赋值操作,这样就相当于2个线程都是在i=1的基础上进行了+1,最终结果还是2。这就造成了线程不安全。

总结volatile不能解决原子性问题,可以通过syn、lock锁来保证。


3、volatile禁止重排优化问题

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

这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

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

由于步骤1和步骤2间可能会重排序,如下:

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

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

  //禁止指令重排优化
private volatile static DoubleCheckLock instance;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未闻花名丶丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值