美文网首页
单例模式

单例模式

作者: LENN123 | 来源:发表于2020-04-15 16:20 被阅读0次

前言

单例模式(singleton)下的类最多只能生成一个实例。单例模式有很多实际用途,比如很多工厂类,生成多个实例是没有意义的。在Spring IOC容器下管理的Bean,很多时候也只有一个实例。单例模式的写法有很多,但这些写法中很多并不是完美的。(这里完美这个概念取决于具体的应用场景,合适的才是的完美的)。本文介绍几种常见的单例模式实现以及它们对应的优缺点。

一、朴素版

  • Singleton01.java
public class Singleton01 {
    private static final Singleton01 INSTANCE = new Singleton01();
    private Singleton01(){}

    public static Singleton01 getInstance() {
        return INSTANCE;
    }
}

上面的 Singleton01.java给出了一个最朴素的单列模式实现,简单好理解。

  1. 通过将构造方法私有化,使得除了这个类自身以外的其他类都无法调用构造方法来生成该类的实例对象。
  2. 设置一个私有的类变量,当类加载到内存中时创建一个该类的唯一实例,其他类要是想获取该类的实例可以通过getInstance()方法来获取这个类变量。
  3. 由于一个类只会被加载的内存中一次,因此这个方法虽然简单,但却是线程安全的。

实验验证一下该单例模式是否起作用。

public class SingletonTest {
    public static void main(String[] args) {
        Singleton01 s1 = Singleton01.getInstance();
        Singleton01 s2 = Singleton01.getInstance();
        System.out.println(s1 == s2);
    }
}

实验结果可以发现每次get到的确实是同一个实例。

true

Process finished with exit code 0

这个方法可以满足绝大多数的应用场景,但缺点是这个类的实例化的过程是在该类被加载到内存中时自动完成的,因此就有人提出了能否当真正需要用到这个单例的时候再实例化呢?于是提出了懒加载的单例模式。(虽然这个理由可以勉强算做缺点,但是Singleton01仍然是一个容易记忆的好方法,推荐使用。)

二、 懒加载版单例模式

  • Singleton02.java
public class Singleton02 {
    private static Singleton02 INSTANCE;
    private Singleton02(){}

    public static Singleton02 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }
}

懒加载版本的核心是在获取单例的getInstance()方法中,只有指向单例的私有类变量INSTANCE为空时(即第一次调用getInstance()方法),才会调用私有的构造方法创建实例,之后再次调用getInstance()方法时则会直接返回已经创建好的实例。但是这个方法有一个很严重的问题,它不是线程安全的,也就是说在多线程的环境下即有可能构造出这个类的多个实例,而达不到单例模式的效果,为什么会这样呢?让我们考虑如下线程A线程B的工作时序图。

引起并发错误的执行流程
  1. 线程A在发现INSTANCEnull时准备创建一个对象实例赋值给INSTANCE
  2. 在线程A创建好实例赋值给INSTANCE之前,线程B也发现INSTACNE = null,于是也开始创建一个对象实例。
  3. 当线程AB运行结束,一共创建了2个实例对象,破坏了单例模式。

让我们用代码模拟一下这个过程,修改Singleton2.java的代码,使其在创建实例前稍作停顿。

public class Singleton02 {
    private static Singleton02 INSTANCE;
    private Singleton02(){}

    public static Singleton02 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }
}

测试代码


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

输出结果

designpattern.Singleton02@24d66a78
designpattern.Singleton02@46ca3f1f
designpattern.Singleton02@36c7f27
designpattern.Singleton02@579ade96
designpattern.Singleton02@619d7c8
designpattern.Singleton02@10bfa816
designpattern.Singleton02@36c7f27
designpattern.Singleton02@6447497b
designpattern.Singleton02@6bce6bde
designpattern.Singleton02@7443edb9

可以发现,10个线程并发的获取单例,但是显然返回的不是同一个实例。
那么既然是一个并发错误,我们可以通过加锁的方式来解决,比如给getInstance方法加上synchronized关键字修饰。

  • 利用加锁的方式解决上述并发错误
public class Singleton02 {
    private static Singleton02 INSTANCE;
    private Singleton02(){}

    public synchronized static Singleton02 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }
}
  • 实验结果
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27
designpattern.Singleton02@36c7f27

现在同一个时刻只有一个线程能拿到getInstance()方法上的锁,从而不会创建处多个实例。这里解决了并发错误,却引入了一个可用性问题。创建实例的过程需要加锁,但是获取实例的时候是不需要加锁的,采用这种方式的话,多个线程需要排队等待获取一个实例,效率很低。 为了解决这个问题,我们要去试着缩小同步代码块的范围以进行优化。

  • 修改代码缩小同步代码块的范围(错误)
public class Singleton02 {
    private static Singleton02 INSTANCE;
    private Singleton02(){}

    public static Singleton02 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton02.class) {
                try {
                    Thread.sleep(2);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                INSTANCE = new Singleton02();
            }
        }
        return INSTANCE;
    }
}

这样子只有在创建实例的过程需要同步,获取已创建实例的过程是不需要同步的。但是这个方法却还是错误,让我们再回头看看之前的时序图,当一个线程获取了同步代码块上的锁的时候,它仍然认为INSTANCE == null是成立的,即便另一个线程已经创建好实例并赋值给INSTANCE了,所以这里我们需要再进行一次校验,也就是我们常说的DCL(双重校验锁)。

三、DCL实现线程安全的单例模式

基于上面的分析,我们可以给出DCL实现单例模式的代码

  • Singleton03.java
public class Singleton03 {
    private static Singleton03 INSTANCE;
    private Singleton03(){}

    public static Singleton03 getInstance() {
        // 第一次校验
        if (INSTANCE == null) {
            synchronized (Singleton03.class) {
                // 第二次校验
                if (INSTANCE == null) { 
                    INSTANCE = new Singleton03();
                }
            }
        }
        return INSTANCE;
    }
}

但事实是以上的代码还是存在瑕疵,因为涉及到指令重排问题,指令重排其实是jvm 进行的一种优化,例如我们有三条指令ABC顺序执行,如果AB的执行顺序对最终的结果不产生影响,并且颠倒次序后某种程度上可以优化程序执行速度,那么jvm会在底层将其优化成BAC的顺序执行。但是有时候我们却不希望发生指令重排,因为这种指令重排在多线程下往往会引起一些意向不到的错误。比如我们上面实现的这个DCL的单例模式、虽然INSTANCE = new Singleton03();在我们看来只有一行代码,在jvm的角度看来却是多条指令组合而成,假设指令如下:

指令1. 首先在堆上划出一分空间
指令2. 对这片内存空间进行初始化,赋予一些初始值
指令3. 把指向这片内存空间的地址赋给INSTANCE变量。

这样顺序执行是没问题的,在第三条指令执行前,INSTANCE都等于NULL
其实仔细想想,我们先把内存空间地址赋值给INSTANCE,再对其进行初始化,本质上最终结果都是一样的。那么jvm就有可能通过指令重排把指令3指令2的顺序颠倒。但是这时候就会出问题了,如果指令3执行结束,INSTANCE已经不为null了,但是由于指令2还未执行,所以没有进行初始化,若此时正好有另一个线程调用getInstance()方法,就会获得一个还未初始化完成的实例,从而发生错误。为了解决这个问题,我们需要给INSTANCE这个变量加上volatile关键字,防止指令重排。

  • 正确的写法
public class Singleton03 {
    private static volatile  Singleton03 INSTANCE;
    private Singleton03(){}

    public static Singleton03 getInstance() {
        // 第一次校验
        if (INSTANCE == null) {
            synchronized (Singleton03.class) {
                // 第二次校验
                if (INSTANCE == null) {

                    INSTANCE = new Singleton03();
                }
            }
        }
        return INSTANCE;
    }
}

相关文章

  • 【设计模式】单例模式

    单例模式 常用单例模式: 懒汉单例模式: 静态内部类单例模式: Android Application 中使用单例模式:

  • Android设计模式总结

    单例模式:饿汉单例模式://饿汉单例模式 懒汉单例模式: Double CheckLock(DCL)实现单例 Bu...

  • 2018-04-08php实战设计模式

    一、单例模式 单例模式是最经典的设计模式之一,到底什么是单例?单例模式适用场景是什么?单例模式如何设计?php中单...

  • 设计模式之单例模式详解

    设计模式之单例模式详解 单例模式写法大全,也许有你不知道的写法 导航 引言 什么是单例? 单例模式作用 单例模式的...

  • Telegram开源项目之单例模式

    NotificationCenter的单例模式 NotificationCenter的单例模式分析 这种单例模式是...

  • 单例模式Java篇

    单例设计模式- 饿汉式 单例设计模式 - 懒汉式 单例设计模式 - 懒汉式 - 多线程并发 单例设计模式 - 懒汉...

  • IOS单例模式的底层原理

    单例介绍 本文源码下载地址 1.什么是单例 说到单例首先要提到单例模式,因为单例模式是单例存在的目的 单例模式是一...

  • 单例

    iOS单例模式iOS之单例模式初探iOS单例详解

  • 单例模式

    单例模式1 单例模式2

  • java的单例模式

    饿汉单例模式 懒汉单例模式

网友评论

      本文标题:单例模式

      本文链接:https://www.haomeiwen.com/subject/cxkcvhtx.html