多线程入门(2)——volatile

136 阅读5分钟

volatile

volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。

volatile作用是在并发编程中保证共享变量的可见性,同时防止这个JVM指令重排。每个线程在运行过程中都有自己的工作内存。因为JMM模型主要是是线程各自的工作内存和主内存。线程运行的时候会将主内存的变量读到工作内存,修改完毕后再写入主存。

volatile关键字修饰的变量值一经修改会立即写入主存,假设有AB两个线程,并且B线程操作volatile修饰的变量的时候,会导致A线程工作内存中的缓存volatile变量无效,A线程发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值,从而保证他的可见性。其中涉及到CPU总线嗅探机制和缓存一致性协议。工作内存和主存之间有一个总线,总线相当于工作内存和主存数据传输的主干道,当其中一个工作内存中的值被修改了并且写回内存以后,其他处理器会被通知,使得各自的缓存失效,线程就必须从主存中去读取最新的数据。

volatile第二个呢就是防止JVM指令重排,因为对程序的优化,JVM和CPU会对指令进行重新排序,但不会影响最后的结算结果,如果指令中有对volatile变量进行操作的指令,那么这条指令相当于一个内存屏障,在它之前的指令和在他之后的指令不允许交换位置,保证到达这条指令前,在他之前的指令已经全部完成。

那么线程A在运行的时候,会将变量的值拷贝一份放在自己的工作内存当中。

那么当线程B更改了变量的值之后,但是还没来得及写入主存当中,线程B转去做其他事情了,那么线程A由于不知道线程B对变量的更改,因此还会一直循环下去。

volatile的实现原理

处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内部的数据读到内部缓存后在进行操作,但操作完之后不知道什么时候会写入内存。

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。

但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。


指令重排

Q:为什么产生指令重排?

因为在确保程序执行结果不会变化的情况下,CUP会将指令重排,以此来提高执行效率,但是在多线程的情况下,指令重排可能会导致并发问题,导致线程之间的共享变量值紊乱,从而影响结果。

编译期不进行指令重排。

经典的例子:DCL(懒汉式双重锁检验)+volatile

public class LazyMan {
    //双重检测锁 才能高并发下安全 懒汉式单例 DCL
    private static volatile LazyMan lazyMan;
    private LazyMan(){}

    public LazyMan getInstance(){
        if (lazyMan == null){
            synchronized (lazyMan) {
                if (lazyMan==null) {
                    return lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

}

内存屏障

happens-before

如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。

Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

image-20210317010023428.png

Java内存模型 (JMM)

image-20210314184754409.png

工作内存可以抽象地理解成 :虚拟机栈
主内存 = 堆+ 方法区

总线嗅探机制+缓存一致性协议

1472972-20200710225123177-483858511.png

注意!缓存一致性协议并不是因为变量被volatile修饰而触发的。因为CUP本身存在这样的协议! 刚接触MESI协议时容易产生误解,以为是volatile关键字触发MESI机制来保证变量的可见性,实际没有这一层因果关系。

MESI保障了多核场景的缓存一致性,是一套固有机制,无论是否声明volatile,只要变量在CPU缓存中就能通过这个协议保障可见性。但反过来,如果没有MESI协议,即使声明了volatile也无法保证变量的可见性;