掘金 后端 ( ) • 2024-04-23 13:24

杨超越-9.jpeg

前言

单例模式是设计模式中最简单但又最常用的的设计模式之一,是很多人学的第一个设计模式。引用百度百科的定义:单例模式创建的类在当前进程中,保证一个类只会被实例化一次,并提供了全局访问点,使用的时候通过单例提供的方法来获取实例。在确保线程安全的前提下,很多时候我们只需要同一个类的一个实例即可,而不是在任何使用的地方都实例化一个新对象,因此只能生成一个实例的模式就是单例模式。

一、基本介绍

1.1 什么是单例模式

  单例模式(Singleton Pattern) 是 Java 设计模式中最简单的设计模式之一,是指在内存中 只会且仅 创建一次对象的设计模式,无论什么时候都要保证这一点。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

  单例模式是指一个类只有一个实例,且该类能自行创建这个实例的一种模式,使用的时候通过单例提供的一个静态方法来获取实例。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型,单例模式就是为了实现全局一个实例的需求,如下图所示。

20240201134022.jpeg

注意:

  • 单例类保证内存里只有一个实例,减少了内存的开销。
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须给所有其他对象提供这一实例

1.2 为什么要用单例模式

  在程序中多次使用同一个对象且作用相同时,对象需要频繁的创建和销毁的时候,而创建和销毁的过程由jvm执行,我们无法对其进行优化,为了防止内存飙升、减少了内存开支,单例模式可以让程序仅在内存中创建一个对象,并提供一个访问它的全局访问点,让所有需要调用的地方都共享这一单例对象。单例模式包含的角色只有一个,就是单例类——Singleton。

graph TD
	A1[method1]-->B
	A2[method2]-->B
	A3[method3]-->B
subgraph Heap
    B[singleton]
    class1
    class2
    class3
    .....
    end
style B fill:#f9f,stroke:#333

  单例模式可以避免对资源的多重占用,避免出现多线程的复杂问题。单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。

1.3 应用场景

  • 在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
  • 当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 应用程序的日志应用,一般都是单例模式实现,只有一个实例去操作才好,否则内容不好追加显示。
  • 多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制。

1.4 单例优缺点

  • 优点:
    1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例。
    2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
    3. 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能,避免对共享资源的多重占用。
  • 缺点:
    1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
    2. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
    3. 单例类的职责过重,在一定程度上违背了"单一职责原则"。
    4. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

二、单例模式的实现方式

  通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。单例模式分为如下两类:

分类 说明 饿汉式 类加载的时候就会创建实例对象。 懒汉式 类加载不会创建实例对象,而是首次使用该对象时才会创建。

2.1 饿汉式单例

2.1.1 静态变量方式

  饿汉式单例模式在类初实话的时候就会进行实例化,简单理解类似于一个"饥饿"的人,在程序启动时就要马上吃饭(创建实例),不管后续是否会真正需要使用这个实例。

  该模式的特点是通过静态修饰符修饰,类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了,在程序调用时直接返回该单例对象即可。这种方式比较常用,调用效率高,在类加载时就创建实例对象,因此不存在 线程安全 的问题。但不管程序用不用,实例都早以创建好,这对内存来说是种浪费,容易产生垃圾对象。

graph TD
    A(( )) --> A1[程序启动] --> A2[类加载] --> A3[创建单例对象] --> A4(( ))
    B(( )) --> B1[适用单例对象] --> B2[返回单例对象] --> A4
    style A fill:#333
    style A4 fill:#f9f,stroke:#333,stroke-width:7px,fill-opacity:0.5
public class HungrySingleton {
    // 创建私有变量 ourInstance,用以记录 Singleton 的唯一实例。
    private static HungrySingleton HungryInstance = new HungrySingleton();
    
    // 把类的构造方法私有化,不让外部调用构造方法实例化
    private HungrySingleton() {}

    // 定义公有方法提供该类的全局唯一访问点,外部通过调用getInstance()方法来返回唯一的实例。
    public static HungrySingleton getInstance() {
        return HungryInstance;
    }
}

  饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。"饿汉式"比起“懒汉式”最大的优点就是没有加锁,执行效率会提高很多,但由于是在类加载时就初始化,容易产生垃圾对象,可能会导致资源浪费,尤其是在实例对象较大或者初始化过程较为复杂的情况下。

2.1.2 静态代码块

这种方式和上面没有基本没有什么区别,都会浪费内存。

public class HungrySingleton {
    private static HungrySingleton instance;

    private HungrySingleton() {
    }

    static {
        instance = new HungrySingleton();
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

2.2 懒汉式单例

2.2.1 懒汉式单例

  懒汉式创建对象的方法是在真正使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象,否则先执行实例化操作。只有在需要时才创建实例对象,节省了系统资源,具备懒加载功能。如下图所示:

graph LR
 start(( )) --> input[适用单例对象]
 input --> conditionA{是否实例化}
 conditionA -- YES --> printA[返回单例对象]
 conditionA -- NO --> printC[实例化对象]
 printC --> printA
 printA --> stop(( ))
 
 style stop fill:#333
 style conditionA fill:#00ffff
 style start fill:#333
 style stop fill:#f9f,stroke:#333,stroke-width:7px,fill-opacity:0.5

  该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。我们创建一个 LazySingleton 类,LazySingleton 类将其构造函数作为私有,并具有自身的静态实例,使用 LazySingleton 类提供静态方法来获取 LazySingleton 对象。

public class LazySingleton {
    // 使用静态变量保存唯一实例
    private static LazySingleton instance;

    // 将构造方法设为私有,防止外部实例化
    private Singleton() {
    }

    // 提供全局访问点,获取唯一实例
    public static LazySingleton getInstance() {
        return instance == null ? new LazySingleton() : instance;
    }
}

  这种方式是最基本的实现方式,这种实现最大的好处就是第一次调用才初始化,如果没有用到该类,那么就不会实例化,从而节约资源,避免内存浪费。但缺点也比较明显,在多线程环境下是不安全的,如果多个线程能够同时进入,并且此时 instance 为 null,那么会有多个线程执行 instance == null,这将导致多次实例化 instance。

2.2.2 懒汉式优化①-线程安全

不就是多线程嘛,加锁!由此,你得出了第一种优化方案,给 getInstance() 方法加锁:

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {
        System.out.println("构造参数初始化");
    }
    
    // 保证每次只有一个线程进入getInstance()方法
    public static synchronized LazySingleton getInstance() {
        return instance == null ? new LazySingleton() : instance;
    }
}

  因为考虑到了多线程机制,实现起来比较麻烦,并且还会出现问题,就算是使用了一定的解救办法(同步、加锁、双重判断)的办法,保证在一个时间点只能有一个线程能够进入该方法,从而避免多次实例化 instance 的问题。但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,性能还是被损耗了,因此懒汉式方法的不推荐使用。

2.2.2 懒汉式优化②-双重检查锁

  很显然,加锁之后,达到了我们的目的,既实现了懒加载特效,也解决了线程安全问题。但是加锁之后,会导致该方法的执行效率特别低,其实就是初始化的时候才会出现线程安全,一旦初始化完成就不存在了。因此,需要把锁的粒度降低。不在方法上加锁,在关键问题上上锁。

public class Singleton {
    private Singleton() {
    }

    /**
     * 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
     */
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            // 加锁
            synchronized (Singleton.class) {
                // 这一次判断也是必须的,不然会有并发问题
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  通过双重检验锁机制来确保只有一个实例对象被创建,解决了懒汉式单例模式的性能、线程安全等问题,看起来是完美无缺的,其实是存在问题的,在多线程下,可能会出现空指针问题。

2.2.3 懒汉式优化③-静态内部类

  静态内部类单例模式由内部类创建,结合了懒汉式和饿汉式各自的优点,利用类加载机制保证了静态内部类只会被加载一次,并初始化其静态属性,从而保证了单例的线程安全性。而只有在需要时才会加载静态内部类,从而实现了延迟加载。

public class Singleton {
    // 私有的静态内部类
    private static class SingletonHolder {
        // 私有的静态变量
        private static final Singleton singletonFour = new Singleton();
    }
    
    private Singleton (){
        System.out.println("构造参数初始化");
    }

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

  代码中增加了内部静态类 SingletonHolder,内部有一个singletonFour 的实例,并且也是类级别的。当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getInstance() 方法从而触发 SingletonHolder.singletonFour 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。看起来像是饿汉式,其实这是懒汉式。因为内部静态类是现在第一次使用的时候才会去初始化,所以SingletonHolder最初并未被初始化。

三、小结

  综上所述,单例模式作为一种简单但又非常重要的设计模式,在实际开发中有着广泛的应用,特别是在Spring框架等大型应用程序中。单例模式虽然简单,但是想写的严谨,还是需要考虑周全。

  当一个类的对象只需要或者只可能有一个时,应该考虑单例模式。如果一个类的实例应该在 JVM 初始化时被创建出来,应该考虑使用饿汉式。如果一个类的实例不需要预先被创建,也许这个类的实例并不一定能用得上,也许这个类的实例创建过程比较耗费时间,也许就是真的没必要提前创建,那么应该考虑懒汉式。非线程安全的懒汉式只能用于非并发的场景,局限性比较大,并不推荐使用。

把今天最好的表现当作明天最新的起点…...~

划重点.gif