专题4_设计模式:单例模式

发布于:2025-09-11 ⋅ 阅读:(19) ⋅ 点赞:(0)

单例模式是设计模式中最常用、最基础的设计模式之一,几乎在所有的项目中都会用到。虽然单例模式很简单,但是想要学好它也是需要花一定的功夫,尤其是某些细节方面需要小心使用。这篇文章将从概念到源码来详细讲解单例模式,从而吃透单例模式。

一、基本概念

1,定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

简单的说,在编码中,让某个类在整个程序中只能new出一个对象,不管在哪调用、什么时候调用,获取的都是同一个实例。

就比如说,操作系统里面的“任务管理器”,不管我打开多少次,显示的都是同一个窗口。

2,目的和适用场景

核心目的

避免资源浪费。有些对象创建过程复杂,反复创建开销很大。单例模式让对象创建一次,避免了反复创建性能开销。
确保状态一致。在某些类的实例对象需要存储全局数据时(比如配置、工具等等),多个实例会导致参数混乱。单例模式可以保证全程序参数信息一致。

适用场景

数据库连接池、线程池、客户端API、系统配置类,日志工具类等等。

3,核心设计思想

单例模式的核心是“控制对象创建”和“提供全局访问”。
控制对象创建:封闭对外的构造方法,禁止外部创建对象,转由自己内部实现对象的创建。
提供全局访问:提供一个开放的静态访问入口,确保始终拿到的是同一个对象。

单例模式符合“单一职责原则”,因为单例类只负责创建自身唯一的实例,且提供了唯一的访问点。不提供多余的业务逻辑。

单例模式的构造要点:
1,构造方法私有化。目的是不能在其他地方实例化该对象。
2,实例唯一。内部仅维护同一个实例,必须通过某些手段确保实例唯一。
3,全局访问点。提供一个public的静态方法访问该实例。

4,单例模式中的角色

单例中仅有一个单例类(Singleton)角色。

5,类图

在这里插入图片描述
就是这么简单的一个结构。

二、常见写法(源码 + 优缺点分析)

单例模式的写法有很多种,我这里提供常见的4种写法:饿汉式、懒汉式、枚举、静态内部类。并分析各自的优缺点。

1,饿汉式

public class Singleton {
	//饿汉式,在静态成员变量直接初始化
    private static final Singleton singleton = new Singleton();
    //私有化构造方法
    private Singleton(){}
    //提供一个开放的全局访问点
    public static Singleton getInstance(){
        return singleton;
    }
}

优点:简单,在程序启动后立即实例化。由JVM保证线程安全。
缺点:程序启动后就初始化,一直就占用内存,如果一直不使用,浪费了内存。
适用场景:单例实例较少的系统,或者程序一启动就需要使用的场景。

2,懒汉式

懒汉式,是将对象的创建过程延迟到使用之前。但是在多线程并发环境下,会出现线程安全问题。可以使用DCL(双重检查锁)来解决。


public class Singleton {
    //2,静态属性通过volatile修饰,保证多线程下的可见性,防止指令重排序
    private static volatile Singleton singleton;
    //1,私有化构造方法
    private Singleton(){}

    public static Singleton getInstance(){
        //1,第一次检查,如果不为空,则直接返回,避免每次加锁,提高性能
        if(singleton == null){
            //2,加锁,保证同一时间只有一个线程进入创建对象
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

为什么要用volatile

使用volatile关键字,是为了防止指令重排序。因为对象创建过程分为3步:
1,分配内存。
2,初始化。
3,引用指向内存。
JVM在执行时,可能会对上面三个指令重排序(形成1->3 ->2),会导致对象已经创建,instance引用不为空,但是还没有实例化。形成半成品状态。

使用volatile关键字,可以保证对象创建的三个过程不会被指令重排序,从而避免了线程获取到中间状态的对象。

为什么需要使用DCL(双重检查锁)

假设不使用DCL,没有第二个if判断,我们来分析看看过程。
1,假设两个线程A、B进入到getInstance方法,执行外层的if判断。
2,接下来如果A线程获取锁,那么A线程会创建一个对象,然后释放锁。
3,接下来线程B获取锁,线程B继续创建对象。最终导致存在两个对象。

因此,需要在synchronized同步代码块中,继续检查一次。

如果是没有第一个if判断,虽然能解决多个实例情况。但是因每次都synchronized加锁,性能低,外层加一个if判断,为了提高性能。

优点:仅在第一次使用时创建(按需创建),节省了内存占用。
缺点:代码复杂了。需要配合volatile、synchronized确保线程安全。
适用于大多数场景,是单例模式的普遍写法。

3,静态内部类

静态内部类写法,是利用“静态内部类只有在使用时才会被加载”的特性,来实现“按需加载”,也能保证线程安全。
这个写法,也是我经常在代码中写的一种方法,看起来比较高级,没有if-else,也不需要加锁。

public class SingletonInnerClass {
    //1,私有化构造方法
    private SingletonInnerClass(){}
    
    //提供对外访问入口
    public static SingletonInnerClass getInstance(){
        return SingletonInner.instance;
    }
    
    //静态内部类
    private static class SingletonInner{
        private static SingletonInnerClass instance = new SingletonInnerClass();
    }
}

优点:按需创建、JVM天然保证线程安全。
缺点:反射下,仍然能够创建多个对象。

4,枚举

public enum SingletonMenu {
    INSTANCE;
    public void method() {
        System.out.println("枚举单例模式");
    }
    //使用方法
    public static void main(String[] args) {
        SingletonMenu.INSTANCE.method();
    }
}

优点:线程安全(JVM保证枚举唯一)、序列化情况下也能保证唯一、反射也不会创建新实例、代码及其简单。

缺点:类初始化时就创建实例,没法做到“按需创建”。

适用于对安全性极高要求的场景。

三、源码中应用

1,spring中的Bean管理

Spring容器管理bean,默认情况下都是单例,其核心是通过DefaultSingletonBeanRegistry实现,相关代码如下:

	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		//第一次非null检查
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				//第二次非null检查
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

其中可以看出,Spring中的单例模式,是用的DCL双重检查锁的写法。

2,JDK中的java.lang.Runtime

再来看Runtime类:

public class Runtime {
    //静态属性
    private static Runtime currentRuntime = new Runtime();
    //提供对外的全局访问点
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    //构造方法私有化
    private Runtime() {}

很熟悉吧,这是饿汉式的单例写法.

四、优缺点总结、避坑

1,总结

优点:
节省资源,避免重复创建对象,检查内存/CPU消耗。
保证状态一致,全程序使用同一个实例。
缺点:
扩展困难,构造方法私有化了,无法扩展。

2,避坑指南

2.1序列化、反序列化破坏单例

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
        oos.writeObject(Singleton.getInstance());

        // 反序列化:会生成新实例
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
        Singleton newInstance = (Singleton) ois.readObject();

        System.out.println(Singleton.getInstance() == newInstance);

上面的示例代码,会输出false。表示单例模式下创建两个对象。
解决方法:在单例模式下增加readResolve()方法:

    private Object readResolve() {
        return singleton;
    }

2.2反射破坏单例

原因:反射可以访问私有构造方法。
看如下示例代码:

        Constructor<Singleton>  constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton1 = constructor.newInstance();
        System.out.println(singleton1 == Singleton.getInstance());

该代码返回false,说明有两个对象了。

解决方法:在狗杂方法中添加非null判断,并抛出异常。或者使用枚举,因为反射无法创建枚举实例。

 if(null != singleton){
      throw new RuntimeException("单例模式不允许多个实例");
  }

五、与其他模式搭配

1,单例 + 工厂

工厂模式是负责创建对象的地方,一般情况下创建对象的地方是不怎么变化的,因此可以将工厂构造成单例模式。

2,单例 + 建造者

建造者模式是将负责对象创建过程分步化,通过一系列分步操作,创建负责的对象。实际开发中一般也是将建造中构建成单例。

六、与相似设计模式对比区别

单例模式容易与“工厂模式”、“享元模式”混淆,接下来就分析其区别。

1,单例与工厂模式

对比维度 单例 工厂
核心目标 保证一个类只有一个实例 解耦对象创建与使用,分离出对象创建过程
实例数量 1 多个
角色数量 1 至少2个(工厂类、产品类)

一句话总结:单例是控制实例数量,工厂是控制实例的创建。

2,单例与享元模式

对比维度 单例 享元
核心目标 一个类只有一个实例 复用多个相似实例,减少内存
实例数量 1 多个
状态管理 分内部状态(共享)、外部状态(不共享)
适用场景 全局唯一对象(工具类) 大量相似对象

网站公告

今日签到

点亮在社区的每一天
去签到