volatile是怎么保障内存可见性以及防止指令重排序的?

1、内存可见性

首先,要明确一下这个内存的含义,内存包括共享主存和高速缓存(工作内存),Volatile关键字标识的变量,是指CPU从缓存读取数据时,要判断数据是否有效,如果缓存没有数据,则再从主存读取,主存就不存在是否有效的说法了。而内存一致性协议也是针对缓存的协议。

内存可见性意思是一个CPU核心对数据的修改,对其他CPU核心立即可见,这句话拆开了理解:

1)、CPU修改数据,首先是对工作内存的修改,也有人说被volatile修饰的变量不会拷贝副本到工作内存,而是直接修改主存,我觉得这个说法是不对的,CPU对数据的修改总是先修改工作内存,然后再同步回主内存,只不过是对被volatile修饰变量的修改,会立刻同步回主内存,假如只有一个线程修改volatile变量,那么这个变量在工作内存的副本会一直有效,CPU也不会每次修改都从主存读取volatile变量,只是每次修改后都会及时更新主存罢了。

2)、对其他核心立即可见,这个的意思是,当一个CPU核心A修改完volatile变量,并且立即同步回主存,如果CPU核心B的工作内存中也缓存了这个变量,那么B的这个变量将立即失效,当B想要修改这个变量的时候,B必须从主存重新获取变量的值。除此之外,即便是单线程读取volatile变量,在变量值不变的情况下,也都是从主存读取。————因此这里面有两种情况,一是已读取的,失效;二是再读取的,从主存读!

说了这么多,volatile有什么用呢?哎,这个作用一定要说清楚,不然很容易忘记!

举个例子:

public class VolatileTest implements Runnable {

    static boolean flag = true;

    @Override
    public void run() {
        while (flag) {
        }
        System.out.println("end......");
    }

    public static void main(String[] args) {
        new Thread(new VolatileTest()).start();
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
        System.out.println("end main......");
    }
}

上面这个例子,子线程会一直卡住,原因就是flag不具备可见性,主线程和子线程刚开始都缓存了flag,且值是true,后来主线程把flag改成了false,但是子线程并不知道,仅此而已,仅此而已!!!!如果把flag用volatile修改,那么主线修改成false后,子线程再次while循环的时候,就会发现它缓存的flag已经失效了,它会去主存重新读取flag的值。

实现的原理一般都是基于CPU的MESI协议(缓存一致性协议),其中E表示独占Exclusive,S表示Shared,M表示Modify,I表示Invalid,如果一个核心修改了数据,那么这个核心的数据状态就会更新成M,同时其他核心上的数据状态更新成I,这个是通过CPU多核之间的嗅探机制实现的。

但是,这样是否就能保证多线程操作一个共享变量的时候,保证线程安全呢?其实不然,否则我怎么说是仅此而已呢!

volatile限定的是从缓存读取时刻的校验,如果两个CPU同时从各自缓存读取一个变量n=1(此时,变量n在各个CPU缓存上都是有效的),并且同时修改了变量n=n+1,再写回缓存,这个时候n的值等于2,而不是等于3。因此,在多线程操作共享变量(例如:计数器)的时候,正确的方式是使用同步或者Atomic工具类。

2、指令有序性

这个涉及到内存屏障(Memory Barrier),内存屏障有两个能力:

a、就像一套栅栏分割前后的代码,阻止栅栏前后的没有数据依赖性的代码进行指令重排序,保证程序在一定程度上的有序性。
b、强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效,保证数据的可见性。

首先,指令并不是代码行,指令是原子的,通过javap命令可以看到一行代码编译出来的指令,当然,像int i=1;这样的代码行也是原子操作。

在单例模式中,Instance inst = new Instance();   这一句,就不是原子操作,它可以分成三步原子指令:

1,分配内存地址;

2,new一个Instance对象;

3,将内存地址赋值给inst;

CPU为了提高执行效率,这三步操作的顺序可以是123,也可以是132,如果是132顺序的话,当把内存地址赋给inst后,inst指向的内存地址上面还没有new出来单例对象,这时候,如果就拿到inst的话,它其实就是空的,会报空指针异常。这就是为什么双重检查单例模式中,单例对象要加上volatile关键字。

内存屏障有三种类型和一种伪类型:

a、lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
b、sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。
c、mfence,即全能屏障,具备ifence和sfence的能力。
d、Lock前缀:Lock不是一种内存屏障,但是它能完成类似全能型内存屏障的功能。

并发三特性总结

特性volatilesynchronizedLockAtomic
原子性无法保障可以保障可以保障可以保障
可见性可以保障可以保障可以保障可以保障
有序性一定程度保障可以保障可以保障无法保障

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值