设计模式-单例模式

单例模式

1. 什么是单例模式?

定义 :单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一的实例。

通俗比喻:一个国家只能有一个皇帝或总统。无论你(程序中的任何部分)在何时何地需要和这位最高领导人沟通,你找到的都是同一个人。单例模式就是为了在整个软件系统中创建这位唯一的"皇帝"。

核心要点

  1. 私有化构造函数:为了防止外部通过 new 关键字随意创建实例,必须将构造函数声明为 private。

  2. 持有静态实例:在类的内部创建一个静态的、属于类本身的实例变量。

  3. 提供公共静态方法:提供一个 public static 方法(通常命名为 getInstance()),作为外界获取这个唯一实例的统一入口。


2. 为什么要使用单例模式?

单例模式主要用于解决需要全局共享且唯一的资源或组件的场景,例如:

  • 配置管理器:整个应用程序共享一份配置信息。

  • 数据库连接池:管理一组数据库连接,避免频繁创建和销毁连接带来的开销。

  • 日志记录器(Logger):所有模块都将日志写入同一个日志文件。

  • 线程池:管理一组工作线程以供整个应用程序使用。

  • 硬件接口访问:如访问打印机、显卡等,通常只需要一个对象来管理。


3. 如何实现单例模式(从简单到完美)

下面我们用 Java 代码来演示几种最经典的实现方式,逐步解决遇到的问题,尤其是线程安全问题。

实现方式一:饿汉式(Eager Initialization)

这是最简单的一种方式,在类加载的时候就立即创建实例。

复制代码
// 饿汉式单例
public class SingletonEager {
    // 1. 在类加载时就创建实例,JVM保证线程安全
    private static final SingletonEager INSTANCE = new SingletonEager();
​
    // 2. 私有化构造函数
    private SingletonEager() {}
​
    // 3. 提供公共的静态方法返回实例
    public static SingletonEager getInstance() {
        return INSTANE;
    }
}
  • 优点

    • 实现简单

    • 线程安全。因为实例是在类加载期间由 JVM 创建的,这个过程天然是线程安全的。

  • 缺点

    • 没有懒加载(Lazy Loading)。不管你用不用这个实例,只要类被加载,实例就会被创建,可能会造成内存浪费。如果这个实例的创建非常耗时,还会拖慢应用的启动速度。
实现方式二:懒汉式(Lazy Initialization)

只有在第一次调用 getInstance() 方法时才创建实例。

复制代码
// 懒汉式 - 线程不安全
public class SingletonLazy {
    private static SingletonLazy instance;
​
    private SingletonLazy() {}
​
    public static SingletonLazy getInstance() {
        // 多线程环境下,这里会出问题!
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}
  • 问题 :这正是你之前问题的场景!如果两个线程同时执行到 if (instance == null),它们都会判断为 true,然后各自创建一个新的 SingletonLazy 实例。这就违背了"单例"的原则。

为了解决上面的问题,最直接的方法就是加锁。

复制代码
// 懒汉式 - 同步方法,线程安全
public class SingletonLazySync {
    private static SingletonLazySync instance;
​
    private SingletonLazySync() {}
​
    // 对整个方法加锁
    public static synchronized SingletonLazySync getInstance() {
        if (instance == null) {
            instance = new SingletonLazySync();
        }
        return instance;
    }
}
  • 优点:解决了线程安全问题。

  • 缺点性能低下。synchronized 关键字给整个方法上了锁。这意味着每次调用 getInstance() 都会发生同步,即使实例已经被创建了。实际上,我们只需要在第一次创建实例时进行同步,后续的调用只是读取,是不需要同步的。

这是对同步方法版的优化,也是面试中最高频的考点。

复制代码
// 双重检查锁定(DCL)
public class SingletonDCL {
    // 关键点1: volatile 关键字
    private static volatile SingletonDCL instance;
​
    private SingletonDCL() {}
​
    public static SingletonDCL getInstance() {
        // 第一次检查:避免不必要的同步
        if (instance == null) {
            // 同步块:只在实例未创建时才进行同步
            synchronized (SingletonDCL.class) {
                // 第二次检查:防止多个线程同时进入同步块
                if (instance == null) {
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

为什么需要双重检查?

  • 第一层 if (instance == null): 为了性能。如果实例已经存在,就直接返回,避免进入昂贵的 synchronized 同步块。

  • 第二层 if (instance == null): 为了线程安全。假设两个线程 A 和 B 都通过了第一层检查。线程 A 拿到锁,创建实例。线程 A 释放锁后,线程 B 拿到锁。如果没有第二层检查,线程 B 会再次创建一个实例。

为什么必须加 volatile? 这是一个非常深入的点。new SingletonDCL() 这个操作在 JVM 中不是原子的,大致可以分为三步:

  1. 为 instance 分配内存空间。

  2. 调用 SingletonDCL 的构造函数,初始化对象。

  3. 将 instance 引用指向分配的内存地址。

由于指令重排序(CPU 和 JIT 编译器的优化),步骤 2 和 3 的顺序可能会被颠倒。即,可能先将引用指向内存地址(此时 instance 就不为 null 了),然后再初始化对象。

如果发生这种情况:

  • 线程 A 执行了步骤 1 和 3,但还没执行步骤 2。

  • 此时线程 B 调用 getInstance(),发现 instance 不为 null(第一层检查),于是直接返回 instance。

  • 但这个 instance 是一个尚未初始化完成的对象,使用它可能会导致程序崩溃。

volatile 关键字可以禁止指令重排序,确保 new 操作的原子性,从而彻底解决 DCL 的隐患。


4. 更优雅的推荐实现方式

虽然 DCL 功能强大,但写法复杂且容易出错。在现代 Java 中,有更好、更简单的实现方式。

实现方式三:静态内部类(Static Inner Class)

这是目前最被推荐的懒汉式实现之一。

复制代码
// 静态内部类实现
public class SingletonStaticInner {
    private SingletonStaticInner() {}
​
    private static class SingletonHolder {
        private static final SingletonStaticInner INSTANCE = new SingletonStaticInner();
    }
​
    public static SingletonStaticInner getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 工作原理

    • 懒加载:只要不调用 getInstance() 方法,SingletonHolder 这个静态内部类就不会被加载,其内部的 INSTANCE 自然也不会被创建。

    • 线程安全 :当 getInstance() 第一次被调用时,JVM 会加载 SingletonHolder 类。类的加载过程和静态变量的初始化在 JVM 内部是天然线程安全的,由 JVM 保证只有一个线程能执行静态初始化块。

  • 优点:兼具了懒汉式的懒加载和饿汉式的线程安全,且实现简单,代码清晰。

实现方式四:枚举(Enum)

这是《Effective Java》作者 Joshua Bloch 极力推崇的方式,也是最简单、最安全的实现。

复制代码
// 枚举实现
public enum SingletonEnum {
    INSTANCE;
​
    // 可以添加普通方法
    public void doSomething() {
        System.out.println("Doing something...");
    }
}
​
// 使用方法:
// SingletonEnum singleton = SingletonEnum.INSTANCE;
// singleton.doSomething();

优点

  • 代码极其简单

  • 天然线程安全,由 JVM 保证。

  • 防止反序列化重新创建新对象。其他几种方式,如果实现了 Serializable 接口,通过反序列化可以创建一个新的实例,从而破坏单例。而枚举类型在序列化和反序列化时,JVM 会有特殊处理,保证返回的是同一个实例。

  • 缺点

    • 不能懒加载(和饿汉式类似)。

    • 可读性上可能对于不熟悉此技巧的开发者来说有点奇怪。


总结

实现方式 线程安全 懒加载 推荐程度 备注
饿汉式 ⭐⭐⭐ 简单,但可能浪费内存
懒汉式(基础版) ⭐ (不应在多线程环境使用) 教学用,展示问题
懒汉式(同步方法) ⭐⭐ 性能差,不推荐
双重检查锁定 (DCL) ⭐⭐⭐⭐ 高性能,但写法复杂,易错(volatile)
静态内部类 ⭐⭐⭐⭐⭐ (强烈推荐) 结合了懒加载和线程安全,代码优雅
枚举 ⭐⭐⭐⭐⭐ (强烈推荐) 最简单,且能防反序列化,功能最完善

在日常开发中,静态内部类枚举是实现单例模式的最佳选择。

相关推荐
李广坤1 小时前
状态模式(State Pattern)
设计模式
李广坤2 小时前
观察者模式(Observer Pattern)
设计模式
李广坤3 小时前
中介者模式(Mediator Pattern)
设计模式
李广坤3 小时前
迭代器模式(Iterator Pattern)
设计模式
李广坤4 小时前
解释器模式(Interpreter Pattern)
设计模式
阿无,7 小时前
java23种设计模式之前言
设计模式
Asort7 小时前
JavaScript设计模式(八):组合模式(Composite)——构建灵活可扩展的树形对象结构
前端·javascript·设计模式
数据智能老司机8 小时前
数据工程设计模式——数据基础
大数据·设计模式·架构
笨手笨脚の10 小时前
设计模式-代理模式
设计模式·代理模式·aop·动态代理·结构型设计模式
Overboom18 小时前
[C++] --- 常用设计模式
开发语言·c++·设计模式