volatile关键字详解


volatile关键字详解

volatile的三个特色

  1. 保证线程之间的可见性
  2. 禁止指令重排
  3. 不保证原子性

可见性

概念

可见性是多线程场景中才讨论的,它表示多线程环境中,当一个线程修改了共享变量的值,其余线程可以知道这个修改。java

为何须要可见性

缓存一致性问题:缓存

public class Test {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();

        new Thread(() -> {
            try {
                //延时2s,确保进入while循环
                TimeUnit.SECONDS.sleep(2);
                //num自增
                mythread.increment();
                System.out.println("Thread-" + Thread.currentThread().getName() +
                        " current num value:" + mythread.num);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "test").start();

        while(mythread.num == 0){ 
            //dead
        }

        System.out.println("game over!!!");
    }
}

class Mythread{
    //不加volatile,主线程没法得知num的值发生了改变,从而陷入死循环
    volatile int num = 0;

    public void increment(){
        ++num;
    }
}

如上述代码,若是不加volatile,程序运行结果以下多线程

不加volatile

加上volatile关键字后,程序运行结果以下ide

加上volatile

解决方向:学习

  • 总线锁:优化

    一次只有一个线程能经过总线进行通讯。(效率低,已弃用).net

  • MESI缓存一致性协议,CPU总线嗅探机制(监听机制)线程

    有volatile修饰的共享变量在编译器编译后进行读写操做时,指令会多一个lock前缀,Lock前缀的指令在多核处理器下会引起两件事情。对象


    (参考下面两位大佬的博客)blog

    https://blog.csdn.net/jinjiniao1/article/details/100540277

    https://blog.csdn.net/qq_33522040/article/details/95319946

    • 每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态, 当处理器对这个数据进行修改操做的时候,会从新从系统内存中吧数据读处处理器缓存行里。

    • 处理器使用嗅探技术保证它的内部缓存,系统内存和其余处理器的缓存在总线上保持一致

    • 写一个volatile变量时,JMM(java共享内存模型)会把该线程对应的本地内存中的共享变量值刷新到主内存;

    • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来从主内存中读取共享变量。

禁止指令重排

指令重排概念

编译器和CPU在保证最终结果不变的状况下,对指令的执行顺序进行重排序。

指令重排的问题

能够与双重检验实现单例模式联系起来看:

首先,一个对象的建立过程可大体分为如下三步:

  1. 分配内存空间
  2. 执行对象构造方法,初始化对象
  3. 引用指向实例对象在堆中的地址

可是在实际执行过程当中,CPU可能会对上述步骤进行优化,进行指令重排

序1->3->2,从而致使引用指向了未初始化的对象,若是这个时候另一个线

程引用了该未初始化的对象(只执行了1->3两步),就会产生异常。

不保证原子性

为何没法保证

具体例子

public class Test {
    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        for(int i = 0; i < 6666; ++i){
            new Thread(() -> {
                try {
                    mythread.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "test").start();
        }
        System.out.println("Thread-" + Thread.currentThread().getName() +
                " current num value:" + mythread.num);
    }
}

class Mythread{
    volatile int num = 0;

    public void increment(){
        ++num;
    }
}

上述代码的运行结果以下图

能够看到,循环执行了6666次,但最后的结果为6663,说明在程序运行过程当中出

现了重复的状况。

解决方案

  1. 使用JUC中的Atomic类(以后会专门写一篇学习笔记进行阐述)
  2. 使用synchronized关键字修饰(不推荐)

volatile保证可见性和解决指令重排的底层原理

内存屏障(内存栅栏)

组成

内存屏障分为两种:Load Barrier 读屏障 和 Store Barrier 写屏障

4种类型屏障

种类 例子 做用
LoadLoad屏障 Load1; LoadLoad; Load2 保证Load1读取操做读取完毕后再去执行Load2后续读取操做
LoadStore屏障 Load1; LoadStore; Store2 保证Load1读取操做读取完毕后再去执行Load2后续写入操做
StoreStore屏障 Store1; StoreStore; Store2 保证Load1的写入对全部处理器可见后再去执行Load2后续写入操做
StoreLoad屏障 Store1; StoreLoad; Load2 保证Load1的写入对全部处理器可见后再去执行Load2后续读取操做

做用

  1. 保证特定操做的执行顺序

    在每一个volatile修饰的全局变量读操做前插入LoadLoad屏障,在读操做后插入LoadStore屏障

  2. 保证某些变量的内存可见性

    在每一个volatile修饰的全局变量写操做前插入StoreStore屏障,在写操做后插入StoreLoad屏障

相关文章
相关标签/搜索