设计模式之单例模式:保证唯一性的设计之道

目录

一、什么是单例模式

二、单例模式的应用场景

三、如何实现单例模式

3.1. 饿汉式

3.2. 懒汉式(线程不安全)

3.3. 懒汉式(线程安全)

3.4. 双检锁方式

3.4.1. 为什么要判空两次

3.4.2. 为什么要用volatile关键字

3.5. 静态内部类方式

四、总结


 

一、什么是单例模式

    单例模式(Singleton Pattern)是一种对象创建型模式(Creational Pattern),它的目的是保证一个类只有一个实例,并提供一个全局访问点。在单例模式中,类自身负责保存它的唯一实例,并且该实例可以通过静态方法获取。由此我们可以发现,单例模式的结构是很简单的:

  1. 实现单例模式的类不可以随意实例化,即此类的构造方法需要是私有的,不可被其他类调用;
  2. 外部类需要使用该类的对象时,通过调用该类的方法来获取该类持有的实例化对象,而不能由外部接口自己创建实例对象。
  3. 单例模式的类的实例仅有一个,且被此类自己持有,即该类内部需要维护一个且仅有一个的该类本身的实例化对象;

图片

二、单例模式的应用场景

单例模式的应用场景有很多,常见的使用单例模式的场景以如下三种为例:

  1. 线程池:线程池只需要一个实例来管理线程资源;

  2. 数据库连接池:保证数据库连接的唯一性,避免资源浪费;

  3. 配置文件读取:保证配置信息的唯一性,避免重复读取配置文件。

三、如何实现单例模式

3.1. 饿汉式

    饿汉式,也可以叫做静态变量方式的单例模式实现方式,即不论什么时候要用,该单例对象总是在创建类的时候就实例化好了单例对象:

public class Singleton {
    //维护私有的全局对象
    private static final Singleton instance = new Singleton();
    //构造方法改为private的,禁止外部调用
    private Singleton() {}
    //提供一个全局访问点,客户端通过此方法获取单例对象
    public static Singleton getInstance() {
        return instance;
    }
}

3.2. 懒汉式(线程不安全)

    上面的饿汉式由于在类加载时就创建出来了单例对象,而在程序的运行过程中,有可能一直都用不到这个单例对象,这样就有可能造成资源的浪费,由此就出现了另一种单例模式的实现方式:懒汉式;懒汉式是在客户端获取单例对象时才真正的去尝试创建单例对象,通过懒加载的方式避免了资源浪费的情况:

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

    上述代码虽然解决了资源浪费的问题,但是这种方式在并发场景下是线程不安全的,比如当线程A和线程B同时执行getInstance()方法时,线程A判断instance为null,于是开始执行new Singleton()实例化单例对象,但是当还没有执行完实例化操作时,线程B也判断instance为null,于是也开始执行new Singleton()实例化单例对象,结果线程A和线程B拿到的都是各自线程中实例化出来的Singleton对象,违反了只有一个实例的原则,所以说这种方式是线程不安全的方式。

3.3. 懒汉式(线程安全)

    那么如何在懒加载的同时又能保证线程安全呢,我们可以通过synchronized关键字来保证多线程模式下的安全性:

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

    以上代码实现了懒加载的同时又保证线程的安全,但是还是有一些瑕疵,由于此种方法是给getInstance()方法加锁来实现的,也就导致不论instance对象是否已创建完成,所有线程都是不能同时调用getInstance()方法的,这就导致了这个方法的并发度很低,性能较差。

3.4. 双检锁方式

    再回想一下普通的懒加载方式线程不安全的原因,我们可以发现,线程不安全的问题其实只发生在实例化这一步,我们只需要保证instance = new Singleton();这一步操作的安全性即可,如果已经实例化过了,则各个线程就没必要再进行抢锁的操作了,那么代码就可以改写为:

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

    通过上述代码,只有单例对象还没有实例化时才会对Singleton类进行加锁操作,既保证了实例化对象的线程安全,又保证了实例化对象之后代码的性能。而上述代码中有两个地方或许有人会比较疑惑,一个是synchronized前后为什么需要判断两次null? 另一个是静态对象instance为什么要用volatile关键字修饰?下面我们分别进行说明。

3.4.1. 为什么要判空两次

    第一次判空很容易理解,如果instance为null,就抢锁,进行对象的实例化操作,那么抢到锁之后为什么还要进行第二次判空呢?假设没有第二次判空,我们来想象一个场景:还是线程A和线程B,它们又要同时执行getInstance()方法了,线程A判断instance为null,于是抢到锁,准备对instance进行实例化,此时线程B又判断instance为null了,于是开始排队,等待锁释放再继续运行,然后线程A实例化instance完成并释放锁返回结果,这时B抢到锁,也开始实例化instance对象了,到这里是不是感觉很熟悉?没错,又回到了线程不安全的问题中去了,而这里解决这个问题的方式就是抢到锁之后对instance对象再进行一次判空,这样线程B在第二次判空时就会判定失败而不再进行单例对象的初始化操作了,这种判空-加锁-判空的操作就被称作双检锁,即双重检查锁(Double-Checked Locking),经常被用来保证线程的安全。

3.4.2. 为什么要用volatile关键字

    实例化对象,也就是instance = new Singleton();这一行代码,在JVM层面会被分解为三步操作:

  1. 为对象预先分配内存空间

  2. 初始化对象

  3. 将引用(即instance)指向分配好的内存空间

    而JVM为了提升指令的执行效率,有可能会对上面的步骤进行指令重排序的操作,即执行顺序有可能从1-2-3重排序为1-3-2,对于单线程来讲,1-2-3还是1-3-2并无区别,但是对于多线程来讲,就可能会出现问题,还是以单例模式的不加volatile关键字的双检锁为例,还是线程A和线程B,它们又又要同时执行getInstance()方法了,线程A判断instance为null,于是抢到锁,开始对instance进行实例化,此时实例化对象的指令重排序为1-3-2了,而当线程A刚执行完1-3时,即引用刚刚指向了一块分配好的内存空间,但是这块内存空间内的对象还没有初始化时,CPU切换到了线程B,此时线程B判断instance不为null,就直接将instance返回给客户端了,但是,instance此时其实还没有被初始化,也就是客户端拿到的其实是一个“半成品”,这显然是有问题的。而volatile关键字的其中一个主要特点就是能够禁止指令重排序,指令不会重排序了,上面这个问题当然也就不会发生了,所以instance是一定要用volatile关键字来修饰的。

3.5. 静态内部类方式

    除了以上三种方式,有没有什么方式又能实现懒加载,又能保证线程安全,同时代码还更简洁呢?答案是有的,那就是通过静态内部类实现单例模式的方法了,这种方法是利用静态内部类在被使用时才会被装载的特性来实现的,这种特性天然地就具备了懒加载的功能,同时创建对象时的线程安全问题又由JVM帮助我们处理好了,代码也就在保证要求的基础上更加简洁了:

public class Singleton {
    private Singleton() {}
    //定义静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

四、总结

    单例模式是一种常见且实用的设计模式,它能够确保一个类只有一个实例,并提供一个全局访问点。通过选择合适的实现方式,我们可以在不同的场景中灵活应用单例模式,提高系统的性能和资源利用率。希望本文能够帮助你更好地理解和应用单例模式,谢谢阅读!

图片

  • 47
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值