透过DCL单例深入理解volatile关键字作用与原理

大家都知道,java中的volatile具有:保证可见性禁止指令重排序的功能,在面试中经常被问到DCL单例中加volatile关键字的作用,今天就此剖析一下它的原理。

首先贴一段DCL的代码:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

在上述代码中:

1.第一个singleton==null的作用是判断是否为空,不为空直接返回单例。

2.添加synchronized关键字的作用是保证只有一个线程参与单例的初始化。

3.第二个singleton==null的作用是保证整个环境中只有一个单例被初始化,避免重复初始化:

譬如:某一时刻有N个线程调用此方法,其中只有一个线程可以进入被synchronized修饰的代码块中,其他N-1个线程都被阻塞在外,当第一个线程初始化单例完成后,释放锁,如果不加singleton==null的话就会造成重复。

那么如果不添加volatile关键字的话,就可能会出现指令重排序的问题;

当然,指令重排序在单线程的中不会有问题,因为指令重排序的定义就是:指令重排序不会影响单线程环境下的结果最终一致性。

其实也就是CPU为了提升执行指令的效率,交换指令的执行顺序,这在单线程环境下没有问题,但可能导致在多线程环境下无法保证一致性。

所以,此处的volatile关键字的作用就是禁止重排序。

那么,为什么有这个问题,我们通过getInstance的字节码来看一下:

 0 getstatic #2 <Singleton;>
 3 ifnonnull 37 (+34)
 6 ldc #3 <Singleton>
 8 dup
 9 astore_0
10 monitorenter
11 getstatic #2 <Singleton;>
14 ifnonnull 27 (+13)
17 new #3 <Singleton>
20 dup
21 invokespecial #4 <Singleton.<init> : ()V>
24 putstatic #2 <Singleton;>
27 aload_0
28 monitorexit
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <Singleton;>
40 areturn

其中,我们只需要看这部分代码就行了,下面这部分代码是singleton==new Singleton()的字节码

17 new #3 <Singleton>
20 dup
21 invokespecial #4 <Singleton.<init> : ()V>
24 putstatic #2 <Singleton;>

1.第17行的new指令代表为singleton对象在内存中开辟一块该对象所占大小的内存空间,在堆中。

2.第20行的dup指令的全称是duplicate,代表在线程栈中再复制一份刚开辟的内存空间的物理地址。为什么要复制物理内存地址?因为21行执行的invokespecial指令会弹出一个,如果只有一个并弹出,后续的引用赋值指令就没办法执行了。

3.第21行的invokespecial指令:调用对象的init方法,也就是无参构造方法。将一个内存地址从栈中弹出。

4.第24行的putstatic指令,赋值引用。

其中,第21行和24行可能会发生指令重排序,从而使singleton变量指向一个半初始化状态的对象,volatile关键字可以禁止指令重排序,从而保证了多线程环境下的结果最终一致性。

那么,volatile的原理是什么捏?

首先说一下,在CPU层面可以使用两种方式实现volatile的功能:

1.lfence(读屏障), sfence(写屏障), mfence(读写屏障),这三个指令貌似只有intel的CPU才有。

2.lock指令:总线锁:在同一时刻只有一个线程能操作内存(主存)中的数据,会导致其他CPU核心中的缓存失效(缓存一致性协议:MESI),缓存锁:锁住缓存行,其它核心不能缓存这个数据。所有cpu都有这个指令。

这是在CPU层面的,那么在JVM中,是通过内存屏障实现的。在虚拟机规范中,该规范要求所有的VM必须实现自己的内存屏障,并且该规范规定了8种不可指令重排序的场景,个人感觉不重要,看看了解即可:

  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  • 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  • volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
  • happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  • 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  • 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  • 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  • 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

在hotspot虚拟机中,内存屏障的实现并没有使用lfence、sfence、mfence这种开销少的指令来实现内存屏障,而是选择了lock来实现。

具体代码在jdk源码中的bytecodeinterpreter.cpp中

   if (cache->is_volatile()) {
            if (support  _IRIW_for_not_multiple_copy_atomic_cpu) {
              OrderAccess::fence();
            }
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
   }
 }

如上,若是amd的cpu,向rsp寄存器加0

若是其他的cpu,向esp寄存器加0

上面的重点是lock指令,但是lock指令后面必需要跟一个指令才能执行,但是lock指令不能和nop指令一起执行,所以只能通过曲线救国,向某个寄存器中加0来实现。

若有错误,请指出,谢谢。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值