7、并发编程中单例模式的解决方案

烟雨 5年前 (2021-06-13) 阅读数 314 #Java并发
文章标签 并发Java

单例模式是一种对象创建型模式,使用单例模式,可以保证为一个类只生成唯一的实例对象。也就是说,在整个程序空间中,该类只存在一个实例对象。

在并发编程中单例模式是否能真的能只创建一个对象呢?如何保障对象的唯一性?通过举例分析一下。

一、饿汉式

public class HungerySingleton {
    //加载的时候就创建对象
    private static HungerySingleton instance= new HungerySingleton();
    private HungerySingleton(){
    }

    //返回实例对象
    public static HungerySingleton getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                System.out.println(HungerySingleton.getInstance());
            }).start();
        }
    }
}

1.1、线程安全性

    instance在类的加载阶段就被实例化了,所以只有一次,保证实例对象的唯一性、线程安全。

1.2、缺点

    对象没有延迟加载(用的时候实例化),创建长时间如果不使用,浪费内存,影响性能。

二、懒汉式

public class HoonSynSingletonDemo01 {
    private static HoonSynSingletonDemo01 instance = null;

    private HoonSynSingletonDemo01() {
    }

    public static HoonSynSingletonDemo01 getInstance() {
        //调用getInstance静态方法,非Null才创建。
        if (null == instance) {
            instance = new HoonSynSingletonDemo01();
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                System.out.println(HoonSynSingletonDemo01.getInstance());
            }).start();
        }
    }
}

线程安全性

调用getInstance静态方法,非Null才创建。不能保证实例对象的唯一性。多线程中可能会并发的访问getSingle()方法。
分析
  1. 一个A线程进来后,判断了null == instance(true),这时B线程抢到CPU资源,也开始判断了null == instance(true)。

  2. 当A线再次抢到CPU资源,就会创建一个HoonSynSingletonDemo01对象,A线程结束。

  3. B线程恢复,也会创建一个HoonSynSingletonDemo01对象。这样就不能保证实例对象的唯一性。

2.1、解决方案1-同步方法

getInstance()方法添加synchronized关键字。
public synchronized static HoonSynSingletonDemo01 getInstance() {
    //调用getInstance静态方法,非Null才创建。
    if (null == instance) {
        instance = new HoonSynSingletonDemo01();
    }
    return instance;
}
并发的问题虽然解决了,线程安全了,懒加载也有了。
但是使用synchronized关键字,使得代码退化到串行执行,影响性能。不建议采用。

2.2、解决方案2-DCL(Double-Check-Locking)

为什么要对instance二次检查呢?先来看一段,只加了同步方法块的代码:
public static HoonSynSingletonDemo01 getInstance() {
    //第一次判断
    if (null == instance) {
        synchronized (HoonSynSingletonDemo01.class){
            instance = new HoonSynSingletonDemo01();
        }
    }
    return instance;
}
分析:
  1. 一个A线程进来后,判断了null == instance(true),这时B线程抢到CPU资源,也开始判断了null == instance(true)。

  2. 当A线再次抢到CPU资源,进入同步代码块,创建一个HoonSynSingletonDemo01对象,A线程结束。同时释放锁。

  3. B线程恢复,进入同步代码块,也会创建一个HoonSynSingletonDemo01对象。只是加synchronized同步代码块,无法保证实例对象的唯一性。

DCL(Double-Check-Locking)双重检查锁,即可解决这个问题

修改getInstance()方法代码如下:
public static HoonSynSingletonDemo01 getInstance() {
    //第一次判断
    if (null == instance) {
        synchronized (HoonSynSingletonDemo01.class){
            //同步代码块中,也要再次判断
            if (null == instance){
                instance = new HoonSynSingletonDemo01();
            }
        }
    }
    return instance;
}
分析:
  1. 一个A线程进来后,判断了null == instance(true),这时B线程抢到CPU资源,也开始判断了null == instance(true)。

  2. 当A线再次抢到CPU资源,进入同步代码块,再次判断了null == instance(true),创建一个HoonSynSingletonDemo01对象,A线程结束。同时释放锁。

  3. B线程恢复,进入同步代码块,再次判断了null == instance(false),不会再创建对象。保证了实例对象的唯一性。

2.3、问题思考

观察如下代码,是否存在问题。
public class HoonSynSingletonDemo02 {
    private int id;
    private static HoonSynSingletonDemo02 instance = null;

    private HoonSynSingletonDemo02() {
        id = new Random().nextInt(100) + 1;                         //(1)
    }

    public static HoonSynSingletonDemo02 getInstance() {
        //第一次判断
        if (null == instance) {                                     //(2)
            synchronized (HoonSynSingletonDemo02.class){            //(3)
                //同步代码块中,也要再次判断
                if (null == instance){                              //(4)
                    instance = new HoonSynSingletonDemo02();        //(5)
                }
            }
        }
        return instance;                                             //(6)
    }

    public void getId(){
        System.out.println("id:" + this.id);             //(7)
    }
}

利用Happen-Before规则分析DCL

happens-before具体的定义
  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前(如果A happens-before B,那么Java内存模型将向程序员保证——>A操作的结果将对B可见,且A的执行顺序排在B之前)。

  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

分析
为了分析DCL,我需要预先陈述上面程序运行时几个事实:
  1. 语句(5)只会被执行一次,也就是HoonSynSingletonDemo02只会存在一个实例,这是由于它和语句(4)被放在同步代码块中被执行的缘故(如果去掉语句(3)处的同步代码块,那么这个假设便不成立了)。

  2. instance只有两种的值,要么为null(也就是初始值),要么为执行语句(5)时构造的对象引用。

  3. 如果getInstance()是初次调用,它会执行语句(5)构造一个HoonSynSingletonDemo02实例并返回,如果getInstance()不是初次调用,如果不能在语句(2)处检测到非空值,那么必定将在语句(4)处检测到instance的非空值,因为语句(4)处于同步块中,对instance的写入。

有的读者可能要问了,既然根据第3条事实,getInstance()总是返回相同的正确的引用,为什么还说DCL有问题呢?这里的关键是尽管得到了HoonSynSingletonDemo02实例对象的正确引用,但是却有可能访问到其成员变量id的不正确值 (HoonSynSingletonDemo02.getInstance().getId()有可能打印id的默认值0,正确应该不为0)。
为也说明上诉情况可能发生(打印id的默认值0),我们只需要说明语句(1)和语句(7)并不存在happen-before关系。
证明
  1. 假设线程1是初次调用getInstance()方法,紧接着线程2也调用了getInstance()方法和getId()方法。

  2. 线程2在执行getInstance()方法的语句(2)时,由于对null == instance并没有处于同步块中,因此线程2可能观察到/观察不到线程1在语句(5)时对instance的写入,也就是说instance的值可能为空/非空。

  3. 先假设instance的值非空,线程2观察到了线程1对instance的写入。这时线程2就会执行语句(6)直接返回这个instance的值。

  4. 然后线程2对这个instance调用getId()方法,该方法是在没有任何同步情况被调用,这时我们无法利用happen-before规则得到线程1的操作和线程2的操作之间的任何有效的happen-before关系。

通过分析线程1的语句(1)和线程2的语句(7)之间并不存在happen-before关系。这就意味着线程2在执行语句(7)完全有可能观测不到线程1在语句(1)处对id写入的值。导致HoonSynSingletonDemo02.getInstance().getId()有可能打印id的默认值0。这就是DCL的问题所在。

DCL问题解决

我们可以将instance声明为volatile,修改(1)处代码如下:
private volatile static HoonSynSingletonDemo02 instance = null;

根据volatile规则,可以得到:线程1的语句(5) ——> 语线程2的句(2)。根据单线程规则,可以得到:线程1的语句(1) ——> 线程1的语句(5)和语线程2的句(2) ——> 语线程2的句(7)。再根据传递规则,可以得到:线程1的语句(1) ——> 线程2的句(7)。这表示线程2能够观察到线程1在语句(1)时对id的写入值,程序能够得到正确的行为。也可以通过final关键字修饰instance,也可以解决这个问题。

2.4、解决方案3-静态内部类

利用classloder的机制来保证初始化instance时只有一个线程。

public class Singleton {
    private static class SingletonHolder{
        public static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.singleton;
    }
}

2.5、解决方案3-Holder模式

声明类的时候,成员变量中不声明实例变量,而放到内部静态类中。这也是广泛的一种单例模式。

public class HolderDemo {
    private HolderDemo(){
    }
    
    private static class Holder{
        private static HolderDemo instance = new HolderDemo();
    }

    public static HolderDemo getInstance(){
        return Holder.instance;
    }
}

2.6、解决方案3-枚举

枚举是一个单例对象,绝对不会出现多个实例。通过如下代码实现懒加载。这样避免使用加锁。
public class EnumSingletonDemo {
    private EnumSingletonDemo(){
    }
    
    private enum EnumHolder{
        INSTANCE;
        private static EnumSingletonDemo instance = null;

        private EnumSingletonDemo getInstance(){
            instance = new EnumSingletonDemo();
            return instance;
        }
    }
    
    //懒加载
    public static EnumSingletonDemo getInstance(){
        return EnumHolder.INSTANCE.instance;
    }
}


版权声明

非特殊说明,本文由Zender原创或收集发布,欢迎转载。

上一篇:6、CAS 下一篇:8、AQS-ReentrantLock

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

作者文章
热门