DCL单例为什么要加volatile?

问题分解

要理清楚这个问题,首先要理解什么是单例;什么是DLC;什么是volatile;以及多线程和实例化对象底层原理。

 

单例是什么?

先贴一段维基百科的定义。

In software engineering,  the singleton pattern is a software design pattern that restricts the instantiation of a class to one object.  This is useful when exactly one object is needed to coordinate actions across the system.  The concept is sometimes generalized to systems that operate more efficiently when only one object exists,  or that restrict the instantiation to a certain number of objects.  The term comes from the mathematical concept of a singleton.

单例模式限制了一个类最多只能有一个实例,为了实现这个要求,通常将单例类的构造方法私有化,于是无法通过外部进行对象实例化操作,然后再在单例类的内部提供一个访问唯一实例的公共方法。

 

单例模式的六种实现方式

饿汉模式

实现难度:★☆☆☆☆

是否懒加载:✘

是否线程安全:✔

推荐指数:★☆☆☆☆

描述:最最最简单的单例实现方式。不加锁就能保住线程安全,效率高;类加载时就完成初始化,浪费内存。

/**
 * 饿汉模式
 */
public class Singleton1 {
    //创建一个对象
    private static Singleton1 intstance = new Singleton1();

    //构造方式私有化
    private Singleton1() {
    }

    //通过提供公共的对外方法获取这个单例对象
    public static Singleton1 getInstance(){
        System.out.println("获取Singleton1对象:"+intstance);
        return intstance;
    }

    //test
    public static void main(String[] args) {
        //单线程-饿汉模式测试
        Singleton1 singleton1 = Singleton1.getInstance();
        //多线程-饿汉模式测试
        for (int i=0 ;i<10 ; i++){
            new Thread(()->{Singleton1.getInstance();}).start();
        }
    }
}

懒汉模式

实现难度:★☆☆☆☆

是否懒加载:✔

是否线程安全:✘

推荐指数:★☆☆☆☆

描述:懒加载的一种简单单例实现。第一次访问对象时才会进行初始化,避免内存浪费;线程不安全,只适用于单线程的场景。

/**
 * 懒汉模式
 */
public class Singleton2 {
    //一开始先不创建对象
    private static Singleton2 instance;

    //构造方法私有化
    private Singleton2() {
    }

    //在获取实例的时候才创建对象
    public static Singleton2 getInstance(){
        try {
            if(instance == null){
                Thread.sleep(1000); //模拟创建实例前的一些耗时操作
                instance = new Singleton2();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("获取Singleton2对象:"+instance);
        return instance;
    }

    //test
    public static void main(String[] args) {
        //单线程-懒汉模式测试
        Singleton2 singleton2 = Singleton2.getInstance();
        //多线程-懒汉模式测试
        for (int i=0 ;i<10 ; i++){
            new Thread(()->{Singleton2.getInstance();}).start();
        }
    }
}

synchronize加锁

实现难度:★★☆☆☆

是否懒加载:✔

是否线程安全:✔

推荐指数:★★☆☆☆

描述:利用synchronize实现线程安全。容易实现,懒加载并且线程安全;synchronize锁影响效率。适用于对getInstance() 的性能要求不高的场景。

/**
 * synchronize锁
 */
public class Singleton3 {
    //一开始先不创建对象
    private static Singleton3 instance;

    //构造方法私有化
    private Singleton3() {
    }

    //在获取实例的时候才创建对象
    public static synchronized Singleton3 getInstance(){
        try {
            if(instance == null){
                Thread.sleep(1000); //模拟创建实例前的一些耗时操作
                instance = new Singleton3();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("获取Singleton3对象:"+instance);
        return instance;
    }

    //test
    public static void main(String[] args) {
        //单线程-synchronize锁测试
        Singleton3 singleton3 = Singleton3.getInstance();
        //多线程-synchronize锁测试
        for (int i=0 ;i<10 ; i++){
            new Thread(()->{Singleton3.getInstance();}).start();
        }
    }

}

DLC双重检验机制

实现难度:★★★★☆

是否懒加载:✔

是否线程安全:✔

推荐指数:★★★★☆

描述:采用双重校验机制在保证线程安全的前提下,保持高性能。适用于对getInstance() 的性能要求较高的场景。

/**
 * 双重校验锁-DLC
 */
public class Singleton4 {
    private  static Singleton4 instance;

    //构造方法私有化
    private Singleton4() { }

    //在获取实例的时候才创建对象
    public static Singleton4 getInstance(){
        try {
            if(instance == null){ //第一层校验
                Thread.sleep(1000); //模拟创建实例前的一些耗时操作
                synchronized(Singleton4.class) {
                    if(instance == null) {//第二层校验
                        instance = new Singleton4();
                    }
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("获取Singleton4对象:"+instance);
        return instance;
    }

    //test
    public static void main(String[] args) {
        //单线程-DLC测试
        Singleton4 singleton4 = Singleton4.getInstance();
        //多线程-DLC测试
        for(int i = 0 ; i < 10 ; i++){
            new Thread(()->{Singleton4.getInstance();}).start();
        }
    }
}
 

静态内部类

实现难度:★☆☆☆☆

是否懒加载:✔

是否线程安全:✔

推荐指数:★★★★★

描述:利用classloader 机制确保初始化instance 时只有一个对象。实现简单,懒加载,线程安全,性能高,《Java Concurrency in Practice》的作者Brian Goetz推荐。

/**
 * 静态内部类的方式
 */

public class Singleton5 {

    private static class SingletonHolder{
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    private Singleton5() {
    }

    public static final Singleton5 getInstance(){
        System.out.println("获取Singleton5对象:"+SingletonHolder.INSTANCE);
        return SingletonHolder.INSTANCE;
    }

    //test
    public static void main(String[] args) {
        //单线程-静态内部类测试
        Singleton5 singleton5 = Singleton5.getInstance();
        //多线程-静态内部类测试
        for (int i=0 ;i<10 ; i++){
            new Thread(()->{Singleton5.getInstance();}).start();
        }
    }
}

枚举

实现难度:★☆☆☆☆

是否懒加载:✔

是否线程安全:✔

推荐指数:★★★★★

描述:简洁,高效,线程安全,自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。《Effective Java》的作者Joshua Bloch推荐。适用于JDK1.5之后的场景。

**
 * 枚举的方式
 */
public enum  Singleton6 {
    INSTANCE;

    private Singleton6(){
        randon = Math.random();
    }

    private  double randon;

    public  double getRandom(){
        return randon;
    }

    //test
    public static void main(String[] args) {
        //单线程-枚举测试
        System.out.println(Singleton6.INSTANCE.getRandom());
        //多线程-枚举测试
        for (int i=0 ;i<10 ; i++){
            new Thread(()->{
                System.out.println(Singleton6.INSTANCE.getRandom());
            }).start();
        }
    }
}

 

volatile是什么?

In computer programming, particularly in the CC++, C#, and Java programming languages, the volatile keyword indicates that a value may change between different accesses, even if it does not appear to be modified.

The Java programming language also has the volatile keyword, but it is used for a somewhat different purpose. When applied to a field, the Java qualifier volatile provides the following guarantees:

  • In all versions of Java, there is a global ordering on the reads and writes to a volatile variable. This implies that every thread accessing a volatile field will read its current value before continuing, instead of (potentially) using a cached value. (However, there is no guarantee about the relative ordering of volatile reads and writes with regular reads and writes, meaning that it's generally not a useful threading construct.)
  • In Java 5 or later, volatile reads and writes establish a happens-before relationship, much like acquiring and releasing a mutex.[8]

在多线程的场景下,volatile关键字表明一个变量表面上没有发生修改,但是在有可能在其他线程的影响下,变量的值已经发生了变化。不同编程语言volatile的作用不尽相同,在Java中,volatile主要有两大特性:线程可见性和禁止指令重排。

 

线程可见性

线程可见性说的是多线程的情况下,线程A对变量V进行了修改,其他线程可以感知变量V的变化。下面的程序模拟了这个场景。

public class TestThreadVisiable {
    /*volatile*/ boolean flag = true;

    public TestThreadVisiable() {
    }

    void test() {
        System.out.println(Thread.currentThread().getName() + ":" + this.flag);

        while(this.flag) {
        }

        System.out.println(Thread.currentThread().getName() + ":" + this.flag);
    }

    public static void main(String[] args) {
        TestThreadVisiable t = new TestThreadVisiable();
        (new Thread(t::test, "Th1")).start();

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException var3) {
            var3.printStackTrace();
        }

        t.flag = false;
    }
}

上述程序中,main线程和Th1线程都访问了同一个变量flag。线程Th1根据flag的值执行while循环,main线程先休眠1s之后,将flag值改成false。如果没有使用volatile修饰flag,Th1线程无法感知main线程对flag的修改,陷入死循环;如果使用volatile修饰flag,1s后Th1线程感知到flag变成false,终止循环。

 

禁止指令重排

写到这里有点饿了,咱们先来做个西红柿炒蛋吧。那么问题来了,西红柿炒蛋是先炒西红柿还是先炒蛋呢?先炒哪个其实都没关系,因为西红柿和炒蛋两个操作并没有必然的关联性,如果咱们有两口锅,炒西红柿和炒蛋同时进行也是完全可以的。这就是西红柿炒蛋版的指令重排。

为了提高程序的运行效率,CPU底层会对指令进行重排序。比如这段代码:a=1; b=2; c=b+a; 由于第一条指令和第二条指令没有关联性,在指令重排的情况下,有可能b=2;先执行,然后再执行a=1;

volatile修饰的语句则禁止CPU这种乱序执行,保证指令执行的顺序性。在硬件级别上,X86平台提供了三种内存屏障来保证指令顺序执行,分别是:lfence - 加载屏障、sfence - 存储屏障、mfence - 全能屏障。但是!!volatile在Hotspot中并不是使用这三种内存屏障实现顺序执行,而是使用Lock前缀。Lock前缀既保证了volatile的线程可见性也保证了禁止指令重排。这里涉及硬件层面就不展开说了。

 

DLC单例为什么要用volatile?

前面讲了DLC单例是如何实现的,首先第一次检验instance是否为空,然后加锁,第二次检验instance是否为空,为空的话进行对象实例化。问题的关键在于对象实例化在汇编层面是多条指令完成的,并且有可能发生指令重排。

public class TestNewInstance {
    public static void main(String[] args) {
        Object o = new Object();
    }
}

将上面的代码编译后找到class文件所在目录,用javap -c TestNewInstance.class命令得到编译后的汇编指令。

       0: new           #2                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: astore_1
       8: return

顺序的情况下,new指令申请了一块内存空间,invokespecial调用构造方法为对象进行初始化,astore_1将变量和新创建的对象关联起来。但是invokespecial和astore_1这两条指令没有关联性,所以astore_1有可能会跑到invokespecial前面执行。

如图,假设两个线程,线程1执行完astore_1时(此时instance已经指向一块内存地址,不为null,但是对象还未完成初始化),CPU切换到线程2执行if(instance==null),结果为false,于是返回了一个不完整的对象。使用volatile禁止指令重排就可以避免这种情况发生。

 

总结

在高并发的情况下,DLC双重检验不能保证由于指令重排导致的线程获取到未完全初始化对象的问题,需要利用volatile禁止重排的特性来保证单例的线程安全。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值