Java单例模式实战指南:从原理到代码案例的完整解析
目录
- 什么是单例模式?为什么需要它?
- 单例模式的几种经典实现方式
- 饿汉式 vs 懒汉式:如何选择?
- 线程安全的单例:DCL与静态内部类
- 枚举单例:最简洁的解决方案
- 实战案例:使用单例管理数据库连接
- 常见问题与避坑指南
什么是单例模式?为什么需要它?
Q:单例模式的核心思想是什么?
A:单例模式确保一个类在JVM中只有一个实例,并提供全局访问点,它常用于管理共享资源(如线程池、缓存、配置对象),避免频繁创建和销毁对象带来的性能开销。

Q:何时必须用单例?举个例子
A:当某个对象需要被整个系统共享且状态唯一时,比如应用配置管理器,如果多次创建,会导致内存浪费或数据不一致,日志记录器如果存在多个实例,日志可能重复写入。
单例模式的几种经典实现方式
在Java中,单例主要通过以下几种方式实现:
代码案例1:饿汉式(线程安全但可能浪费内存)
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {} // 私有构造防止外部实例化
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
优点:简单、线程安全(JVM加载时即创建)。
缺点:无论是否使用,类加载时就创建实例,可能增加启动时间。
代码案例2:懒汉式(非线程安全版)
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton(); // 多线程可能创建多个
}
return instance;
}
}
问题:多个线程同时调用getInstance()可能创建多个实例,需加锁。
饿汉式 vs 懒汉式:如何选择?
Q:项目启动慢,优先选哪种?
A:懒汉式,它延迟初始化直到真正调用,适合创建成本高或依赖外部配置的对象,饿汉式则适合轻量级且一定会被使用的对象。
Q:懒汉式如何保证线程安全?
A:给getInstance()方法加synchronized关键字,但直接加在方法上会导致每次调用都同步,性能差,优化方案见下一节。
线程安全的单例:DCL与静态内部类
方案1:双重检查锁定(DCL)
public class DCLSingleton {
private static volatile DCLSingleton instance; // volatile保证可见性
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DCLSingleton.class) {
if (instance == null) { // 第二次检查
instance = new DCLSingleton();
}
}
}
return instance;
}
}
关键点:volatile禁止指令重排,避免部分构造的对象被其他线程读取,这是目前最常用的线程安全懒汉式。
方案2:静态内部类(基于类加载机制)
public class InnerClassSingleton {
private InnerClassSingleton() {}
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优势:由JVM保证线程安全,且延迟加载(只有调用getInstance时才会加载内部类)。
枚举单例:最简洁的解决方案
Q:为什么推荐枚举实现单例?
A:枚举天生具备序列化安全、线程安全,且代码极简,Joshua Bloch在《Effective Java》中强烈推荐。
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("枚举单例方法调用");
}
}
使用场景:需要序列化/反序列化单例时,枚举自动防御反射攻击和克隆破坏。
实战案例:使用单例管理数据库连接
假设你的应用需要全局共享一个数据库连接池,实现如下:
import java.sql.Connection;
import java.sql.DriverManager;
public class DBConnectionManager {
private static volatile DBConnectionManager instance;
private Connection connection;
private DBConnectionManager() {
try {
// 加载驱动并创建连接(实际请用连接池)
Class.forName("com.mysql.cj.jdbc.Driver");
this.connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb", "user", "pass");
} catch (Exception e) {
throw new RuntimeException("连接数据库失败", e);
}
}
public static DBConnectionManager getInstance() {
if (instance == null) {
synchronized (DBConnectionManager.class) {
if (instance == null) {
instance = new DBConnectionManager();
}
}
}
return instance;
}
public Connection getConnection() {
return connection;
}
}
调用方式:DBConnectionManager.getInstance().getConnection(),整个应用共享同一个连接,避免反复创建。
常见问题与避坑指南
Q1:反射能破坏单例吗?如何防御?
A:通过反射调用私有构造器可以创建新实例,防御方案:在构造器中检查实例是否已存在,若存在则抛出异常;或用枚举单例(天然免疫反射)。
Q2:序列化会破坏单例吗?
A:如果类实现了Serializable,反序列化时会创建新对象,解决方案:添加readResolve()方法返回现有单例实例;或使用枚举单例。
Q3:单例与Spring的Bean作用域是什么关系?
A:Spring默认Bean也是单例,但由容器管理,无需自行编码,但理解原生单例有助于理解Spring底层原理。
Q4:单例在分布式系统下是否失效?
A:单例仅保证一个JVM内唯一,分布式场景需使用其他方案(如Redis锁、数据库表锁),但这是另一个话题。
单例模式是Java开发者必须掌握的经典设计模式之一,从饿汉式到枚举,每种实现都有其适用场景,在实际开发中,优先推荐静态内部类或枚举实现,既保证线程安全,又避免性能开销,理解这些代码案例,你将能在面试和项目中游刃有余地使用单例模式。