单例模式
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) |
静态内部类 | 是 | 是 | ⭐⭐⭐⭐⭐ (强烈推荐) | 结合了懒加载和线程安全,代码优雅 |
枚举 | 是 | 否 | ⭐⭐⭐⭐⭐ (强烈推荐) | 最简单,且能防反序列化,功能最完善 |
在日常开发中,静态内部类 和枚举是实现单例模式的最佳选择。