volatile的可见性和禁止重排序

1.volatile概述

volatile是一个关键字,它能保证变量在多线程之间的可见性,禁止CPU执行时进行指令重排操作(内存屏障)从而能保证有序执性,但是它并不能保证原子性。

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。

lock前缀指令相当于一个内存屏障(也称为内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时,不会把后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;也即是,在执行到内存屏障这个指令时,在他前面的操作已经完成。

(2)它会强制将缓存的修改操作立即写入主存。

(3)如果是写操作,他会导致其他CPU中对应的缓存无效。

所以

2.volatile的可见性

  • 可见性:

volatile的功能就是,将被修改的变量,在被修改后可以立即同步到主内存,被修改的变量在每次被使用之前都从主内存刷新,其实本质也是通过内存屏障来实现可见性。

写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写会主存,读内存屏障(Load Memory Barrier)可以促使处理器处理invalidate queue(失效队列)。进而避免由于store buffer和invalidate queue的非实时性带来的问题。

代码:

public class VolatileDemo {
    //定义一个共享变量
    private static boolean flag;

    public static void main(String[] args) {
        System.out.println("开启main线程。。。");

        Thread t1 = new Thread(() -> {
            while (!flag) {
                //System.out.println("值未改变,当前值为:" + flag);
            }
            System.out.println("线程:" + Thread.currentThread().getName() + "感知到了flag值的变化,当前值为:" + flag);
        }, "t1");
        t1.start();

        //主线程睡眠100毫秒
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread t2 = new Thread(() -> {
            flag = true;
            System.out.println("线程:" + Thread.currentThread().getName() + "将值改为:true");
        }, "t2");
        t2.start();
    }

}

上面代码很简单 定义一个共享变量(此时没有加volatile修饰),开启两个线程,一个线程修改变量的值,另外一个线程一直去循环获取修改后的值。

执行结果:

当我执行这段代码的时候就会发现,程序一直在执行没有结束,线程 T1一直没有获取到线程T2修改后的值。(其实一直等下去线程T1也是能获取到最新的flag值,不过不知道要等到猴年马月,我没有继续等哈哈)

加上volatile关键字之后再执行刚才的代码。

 在让我们看一下执行结果

很显然T2将 flag值 false--->true,T1立即就获取到了更新后的值。

2.1总结

        通过上述例子,我们会发现当变量没有被volatile 关键字修饰的时候,多线程对变量的操作结果很难被其他线程发现。当变量被volatile关键字修饰以后,多线程操作变量的结果会瞬间被其他线程发现,这也就是本节第二个知识点:volatile支持可见性。

3.volatile禁止指令重排

        volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。

硬件层的内存屏障

Intel硬件提供了一系列的内存屏障,主要有: 

1. lfence,是一种Load Barrier 读屏障 

2. sfence, 是一种Store Barrier 写屏障 

3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力 

4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad; Load2保证load1的读取操作在load2及后续读取操作之前执行
StoreStoreStore1; StoreStore; Store2

在store2及其后的写操作执行前,保证store1的写操作已刷新到

主内存

LoadStoreLoad1; LoadStore; Store2在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoadStore1; StoreLoad; Load2

保证store1的写操作已刷新到主内存之后,load2及其后的读操作

才能执行

更直观的可以用图形来看:

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • StoreStore屏障将保障上面所有的普通写操作结果在volatile写之前会被刷新到主内存->普通写操作对其他线程可见
  • StoreLoad屏障的作用是避免volatile写操作与后面可能有的volatile读/写操作重排序
     

  • 在每个volatile读操作的后面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障
  • LoadStore屏障用来禁止编译器把上面的volatile读操作与下面的普通读写操作重排序

 

 3.1总结 

        volatile只能保证可见性和有序性但不能保证原子性,原子性需要通过Synchronized这样的锁机制实现

四:volatile不支持原子性


public class Test1 {
    //定义一个对象
    private static int number = 0;
 
    public static void main(String[] args) {
 
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        number++;
                    }
                }
            });
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(number);
    }
}

上面这段代码也是很简单的一段代码,相信大家都能看明白。。按照正常情况输出结果都应该为 :10*10000=1000000。

接下来让我们执行一下看看效果,第一次执行变量number没有添加volatile关键字修饰,我们执行多次发现最终结果小于预期结果

第二次执行,这次我们使用volatile来修饰number,执行结果如下:

结果依然小于逾期结果

下面看两组代码,其实效果是一样的

代码一:

public class Test2 {
    //定义一个对象
    private volatile static int number = 0;
    private static Object object = new Object();
 
    public static void main(String[] args) {
 
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //我们使用synchronized给下面的代码加上一个同步锁
                    synchronized (object) {
                        for (int j = 0; j < 10000; j++) {
                            number++;
                        }
                    }
                }
            });
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(number);
    }
}
 

代码二:

public class Test2 {

    //定义一个对象
    private static int number = 0;
    
    //我们使用synchronized给下面的方法加上一个同步锁
    public synchronized static void add() {
        for (int j = 0; j < 10000; j++) {
            number++;
        }
    }

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    add();
                }
            });
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(number);
    }

}

两组代码,不管执行多少次,执行结果都是100000,与逾期结果一样

看代码我们发现我们使用到了Synchronized,它是干什么的?

Synchronized实现了线程同步锁机制,它能保证原子性。Synchronized是JVM的内置锁 ,类似的还可以使用ReentrantLock进行加锁。它们之间有什么区别,我们该怎么选择?

4.1总结

volatile不能保证原子性,要想保证原子性我们要使用锁机制。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值