单例模式
1. 什么是单例模式?
定义:单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一的实例。
通俗比喻:一个国家只能有一个皇帝或总统。无论你(程序中的任何部分)在何时何地需要和这位最高领导人沟通,你找到的都是同一个人。单例模式就是为了在整个软件系统中创建这位唯一的“皇帝”。
核心要点:
私有化构造函数:为了防止外部通过 new 关键字随意创建实例,必须将构造函数声明为 private。
持有静态实例:在类的内部创建一个静态的、属于类本身的实例变量。
提供公共静态方法:提供一个 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 中不是原子的,大致可以分为三步:
为 instance 分配内存空间。
调用 SingletonDCL 的构造函数,初始化对象。
将 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) |
静态内部类 | 是 | 是 | ⭐⭐⭐⭐⭐ (强烈推荐) | 结合了懒加载和线程安全,代码优雅 |
枚举 | 是 | 否 | ⭐⭐⭐⭐⭐ (强烈推荐) | 最简单,且能防反序列化,功能最完善 |
在日常开发中,静态内部类和枚举是实现单例模式的最佳选择。