设计模式之单例模式

定义

确保一个类只有一个实例,并为整个系统提供一个全局访问点 (向整个系统提供这个实例)。

类型

创建型

为什么要用单例模式?

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

使用单例模式的好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

单例模式的三个要点:

  1. 构造方法私有化;
  2. 实例化的变量引用私有化;
  3. 获取实例的方法共有

角色

Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。

单例模式的实现方式:

1.饿汉式(线程安全)

public class Singleton {

    private static Singleton singleton  = new Singleton();
    
    private Singleton(){
    }

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

只能通过getInstance()来获取已经new好的实例,所以没有线程安全问题

优点:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全
缺点:没有懒加载,启动较慢;如果从始至终都没使用过这个实例,则会造成内存的浪费。

1.1饿汉式变种(线程安全)

将类实例化的过程放在了静态代码块中,在类装载的时执行静态代码块中的代码,初始化类的实例。

public class Singleton {

    private static Singleton singleton;

    static{
        singleton = new Singleton();
    }
    
    private Singleton(){
    }

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

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

单重检查(线程不安全),只检查有没有被实例化

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

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

优点:懒加载,启动速度快、如果从始至终都没使用过这个实例,则不会初始化该实例,可节约资源
缺点:多线程环境下线程不安全。if (singleton == null) 存在竞态条件,可能会有多个线程同时进入 if 语句,导致产生多个实例

测试,使用原子类AtomicInteger来统计实例化的个数,写在单例类的构造函数里面,每次调用构造函数,原子类都加一。

public class Singleton {

    private static AtomicInteger atomicInteger = new AtomicInteger();
    private static Singleton singleton;

    private Singleton() {
        atomicInteger.getAndAdd(1);
    }

    public static AtomicInteger getCount() {
        return atomicInteger;
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("出错啦!");
            }
            singleton = new Singleton();
        }
        return singleton;
    }
}

Test类

public class Test {

    private static final int THREAD_NUMBER = 10;

    public static void main(String[] args) throws InterruptedException {
        
            Thread[] threads = new Thread[THREAD_NUMBER];
            for (int i = 0; i < THREAD_NUMBER; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Singleton singleton = Singleton.getInstance();
                    }
                });
                threads[i].setName("线程"+i);
                threads[i].start();
            }
            for (int i = 0; i < THREAD_NUMBER; i++) {
                threads[i].join();
            }
            System.out.println("线程全部执行完成!");
            System.out.println("创建了 "+Singleton.getCount()+" 次单例模式类实例");
    }

}

如图所示,创建了10个实例,一个线程创建了一个。
在这里插入图片描述

只需要给getInstance()方法加synchronized,每次只有一个线程能够进入该代码块,就能解决重复创建问题,但是synchronized在多线性竞争下会升级成重量级锁,并发性能比较差。

    public synchronized static Singleton getInstance() {
        if (singleton == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("出错啦!");
            }
            singleton = new Singleton();
        }
        return singleton;
    }

但是我们注意到,多线程引发的问题,只涉及到new Singleton()这个代码,所以,我们可以缩小synchronized同步代码的范围,不修饰整个方法,只修饰new Singleton()

    public static Singleton getInstance() {
                if (singleton == null) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        System.out.println("出错啦!");
                    }
                    synchronized (Singleton.class){
                        singleton = new Singleton();
                    }
                }
        return singleton;
    }

但是这样还是会存在线程不安全问题,要通过如下方式解决

3.双重检查锁(线程安全)

用volatile修饰instance,再使用双重检查锁,这种单例模式类创建实例是线程安全的。
为什么需要用volatile修饰instance?
简单来说,一个new 方法背后会执行多个指令,由于 JVM 具有指令重排的特性,jvm执行指令时,会重排指令来加速运行速度, 在多线程环境下可能出现 singleton 已经赋值但还没初始化的情况,导致一个线程获得还没有初始化的实例。
volatile 关键字的作用:

  1. 保证了不同线程对这个变量进行操作时的可见性
  2. 禁止进行指令重排序
public class Singleton {

    private static AtomicInteger atomicInteger = new AtomicInteger();
    private static volatile Singleton singleton;
    private Singleton() {
        atomicInteger.getAndAdd(1);
    }
    public static AtomicInteger getCount() {
        return atomicInteger;
    }
    public static Singleton getInstance() {
            if (singleton == null) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("出错啦!");
                }
                synchronized (Singleton.class){
                    if(singleton==null){
                        singleton = new Singleton();
                    }
                }
            }
        return singleton;
    }
}

为什么加锁之后,还需要再判断一次是否为null呢,也就是为啥要用双重检查锁呢?
比如有两个或多个线程都通过了第一次判断==null,然后第一个线程获取锁,进入同步代码区,new 一个Singleton实例,第一个线程释放锁之后,然后第二个,第三个,第四个…线程进去,如果不设置第二次判断null,就会再new一个实例出来。所以必须有两次判断。

优点:线程安全;延迟加载;效率较高。

4.静态内部类式(线程安全)

/**
 * @Author: codingXT
 * @Date: 2021-12-04-22:34
 * @Description:  静态内部类式实现单例模式
 */
public class Singleton {

    private Singleton(){
    }

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

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

优点:线程安全,延迟加载,效率高。
静态内部类的方式利用了类装载机制来保证线程安全,只有在第一次调用getInstance方法时,才会装载InnerClas内部类,完成Singleton的实例化,所以也有懒加载的效果。

5.枚举式(线程安全)

/**
 * @Author: codingXT
 * @Date: 2021-12-04-23:19
 * @Description:  枚举类实现单例模式
 */
public enum Singleton {

    INSTANCE;
    private Resource resource;

    private Singleton(){
        resource = new Resource();
    }

    public Resource getInstance(){
        return resource;
    }
    
}

Test类

public class Test {

    public static void main(String[] args){
        Resource resource = Singleton.INSTANCE.getInstance();
        Resource resource2 = Singleton.INSTANCE.getInstance();
    }
}

上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 Singleton.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。

优点:通过JDK1.5中添加的枚举来实现单例模式,写法简单,且不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

《Effective Java》:单元素的枚举类型已经成为实现Singleton的最佳方法。

放一张总结图,图片来源于:https://www.pdai.tech/md/dev-spec/pattern/2_singleton.html
在这里插入图片描述

单例模式总结:

优点:

  • 由于单例模式只生成了一个实例,所以能够节约系统资源、减少性能开销、提高系统效率
  • 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

缺点:

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

适用场景:

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器(比如数据库的主键不能重复,因此该序列号生成器必须具备唯一性,可以通过单例模式来实现。),或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  • 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式

Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。

Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码如下

// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
        synchronized (this.singletonObjects) {
            // 检查缓存中是否存在实例  
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                //...省略了很多代码
                try {
                    singletonObject = singletonFactory.getObject();
                }
                //...省略了很多代码
                // 如果实例对象在不存在,我们注册到单例注册表中。
                addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }
    //将对象添加到单例注册表
    protected void addSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

            }
        }
}

我们上述的多种单例的创建方式有很多都存在漏洞,被攻击时会产生多个对象,破坏了单例模式。

  1. 序列化破坏单例模式以及如何防御
  2. 反射破坏单例模式以及如何防御
  3. 为什么要用枚举类实现单例模式(避免反射、序列化问题)

References:

  1. https://whirlys.blog.csdn.net/article/details/85965063

  2. https://blog.csdn.net/qq_37960603/article/details/104048354

  3. https://blog.csdn.net/yy254117440/article/details/52305175

  4. https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/singleton.html

  5. https://javaguide.cn/systemdesign/framework/spring/Spring%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E6%80%BB%E7%BB%93/#%E5%B7%A5%E5%8E%82%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F

  6. https://www.pdai.tech/md/dev-spec/pattern/2_singleton.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值