Volatile指令重排详解

一、Volatile不可见性

Volatile关键

Volatile指令重排详解_第1张图片

 Volatile指令重排详解_第2张图片

 二、CPU的乱序执行

CPU在进行读等待的同时执行指令,是CPU乱序的根源,不是乱,而是提高效率 

Volatile指令重排详解_第3张图片

三、对象的创建过程

Volatile指令重排详解_第4张图片

 Volatile指令重排详解_第5张图片

对象的创建过程:

创建->初始化->建立连接

1.先申请内存,赋值默认值0

2.构造方法赋值初始值,8

3.建立连接,t->T

Volatile指令重排详解_第6张图片

Volatile指令重排详解_第7张图片

 Volatile指令重排详解_第8张图片

四、DCL单例

4.1 DCL单例代码

DCL单例模式

public class Singleton {
    private volatile static Singleton instance = null;
    public  static Singleton getInstance() {
        if(null == instance) {
            synchronized (Singleton.class) {
                if(null == instance) {
                    instance = new Singleton();
                }
            }
        }

        return instance;

    }
}

4.2 DCL单例(Double Check Lock)到底需不需要volatile

Volatile指令重排详解_第9张图片

 synchronized本身就可以支持线程可见

 看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个if中就返回了,因此不会走到同步块中,已经完美了吗?

  如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

  1)给instance实例分配内存;

  2)初始化instance的构造器;

  3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)

  如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:

  a)给instance实例分配内存;

  b)将instance对象指向分配的内存空间;

  c)初始化instance的构造器;

  这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。

  具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)

  根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量

将变量instance使用volatile修饰即可实现单例模式的线程安全。

五、内存屏障

5.1 CPU层面如何禁止重排序

内存屏障

对某部分内存做操作时前后添加的屏障,屏障前后的操作不可以乱序执行

Volatile指令重排详解_第10张图片

5.2 JSR内存屏障

Volatile指令重排详解_第11张图片

 Volatile指令重排详解_第12张图片

 5.3 JVM层面volatile实现细节

 Volatile指令重排详解_第13张图片

 总之,volatile读写前后,都加屏障

5.4 hanppens-before原则则

JVM规定重排序必须遵守的规则,下面的几种,必须加屏障,禁止指令重排

5.5 as if serial

不管如何重排序,单线程执行结果不会改变,不影响数据的最终一致性

六、volatile如何解决指令重排序

1: volatile i

2: ACC_VOLATILE

3: JVM的内存屏障:屏障两边的指令不可以重排,保障有序。

4:hotspot实现

bytecodeinterpreter.cpp

int field_offset = cache‐>f2_as_index();
    if (cache‐>is_volatile()) {
        if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
            OrderAccess::fence(); // fence屏障
}

orderaccess_linux_x86.inline.hpp

inline void OrderAccess::fence() {
    if (os::is_MP()) {
        // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
        __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
        __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
    }
}

lock addl

LOCK用于在多处理器中执行指令时对共享内存的独占使用。

它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。另外还提供了有序的指令无法越过这个内存屏障的作用。

七、缓存

7.1 计算机的组成

Volatile指令重排详解_第14张图片

 7.2 存储器的层次结构

Volatile指令重排详解_第15张图片

 7.3 从CPU的计算单元(ALU)到:

Volatile指令重排详解_第16张图片

 7.4 多核CPU

Volatile指令重排详解_第17张图片

 按块读取,程序局部性原理,可以提高效率,充分发挥总线CPU针脚等一次性读取更多数据的能力。

缓存行:

缓存行越大,局部性空间效率越高,但读取时间慢缓存行越小,局部性空间效率越低,但读取时间快取一个折中值,目前多用:64字节

 Volatile指令重排详解_第18张图片

 Volatile关键、视频教程

你可能感兴趣的:(JUC)