volatile的底层原理

一.线程间可见

public class VolatileTest {
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
                //do sth
            }
            System.out.printf("end");

        }, "子线程").start();
        Thread.sleep(1000);
        flag =false;
    }

}

如果没有在flag上加上volatile关键字,则主线程改变了flag的值,但是子线程中flag依然为true,一直在循环。只有加上volatile关键字,才保证了线程间的可见性。

根据Java的线程模型,flag在我们的主内存中,每个线程是运行在CPU里面的,每个线程有可能占用不同的CPU,这个CPU去读内存内容的时候,它会把这个值拷贝一份到自己的缓存中。子线程在运行时一直读缓存的值,不会主动再去内存中拿,即便主线程把flag的值写回到主内存了,不好意思,由于你没有通知其他线程,则是不会去主内存中读这个值的。因此它会一直读缓存。加上volatile关键字的意思,谁要是改了这块,马上通知其他线程,要重新读主内存,不能够读自己的缓存。

我们结合CPU和内存结构来具体看下:

volatile的底层原理_第1张图片

假如说X被第一个CPU改为1,那我们怎么通知另外一个CPU,告诉它这个值已经改了,读的时候需要去内存再取。这就出现了缓存一致性协议。其中MESI是IntelCPU的缓存一致性协议。

volatile的底层原理_第2张图片

Modified指这个缓存行被我改过了,Exclusive指这个缓存行被我独占,Shared指这个缓存行大家共享,Invalid指这个缓存行已经失效了,所以MESI协议其实就是这四种状态之间互相通知和转换。一个CPU改了缓存行,只要通知另外一个CPU告诉它这个缓存行失效了,要是重新想用的话,麻烦去主内存那里再读回来。

如果X和Y都加上volatile关键字了,因为它们在同一个缓存行上,当CPU1改了X,马上就会通知CPU2缓存行失效,要重新读内存;同理,当CPU2改了y,也马上通知CPU1缓存行失效,要重新读内存。这就带来了效率的降低。

 

二.禁止指令重排

一般CPU为了提高效率,会对指令进行重排,也就是所谓乱序执行。

volatile的底层原理_第3张图片

结合对象的创建过程来说一下,

volatile的底层原理_第4张图片

第一步new的时候,先在内存中申请一块空间,成员变量m会设置为默认值0(不是8),接着执行invokespecial,就是调用T的构造方法,只有调用构造方法后,才会把m的值设置为8,一般我们把m=0称为半初始化状态,把m=8称为全初始化状态。最后astore_1就把t和new出来的东西建立了连接。

public class SingleTest {
    private static volatile SingleTest INSTANCE;
    private SingleTest(){

    }
    public static SingleTest getInstance(){
        if (INSTANCE == null) {
            synchronized (SingleTest.class) {
                //双重检查锁
                if (INSTANCE == null) {
                    INSTANCE = new SingleTest();
                }
            }
        }
        return INSTANCE;
    }
}

双重检查锁(Double Check Lock) 即DCL单例到底需不需要volatile?

volatile的底层原理_第5张图片

恰好在这个时候发生指令重排,

volatile的底层原理_第6张图片

发生指令重排,先建立了连接,t和半初始化的对象建立了连接

所以要加上volatile!

那么volatile在底层到底怎么禁止指令重排的?

1.内存屏障等系统原语

2.锁总线

内存屏障就是夹在两条指令之间的、不允许两条指令换顺序的这么一个东西。

JVM级别的内存屏障:

volatile的底层原理_第7张图片

LoadLoad屏障指上面一个Load指令,下面一个Load指令,这两个指令不可以换。其他三个同理。

volatile的底层原理_第8张图片

顺便提下happens-before原则(有些地方是不可以进行重排序的)

volatile的底层原理_第9张图片

上面提的都是JVM要求的规范,那么底层怎么实现这种要求?

一种是和特定CPU相关的,有的CPU支持如下指令。

volatile的底层原理_第10张图片

另一种就是万能的,锁总线。

你可能感兴趣的:(java)