设计模式-单例模式

发布于:2025-06-20 ⋅ 阅读:(20) ⋅ 点赞:(0)

单例模式

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)
静态内部类 ⭐⭐⭐⭐⭐ (强烈推荐) 结合了懒加载和线程安全,代码优雅
枚举 ⭐⭐⭐⭐⭐ (强烈推荐) 最简单,且能防反序列化,功能最完善

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


网站公告

今日签到

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