设计及模式之单例模式
前言
Github:https://github.com/HealerJean
一、单例模式
单例模式是一种创建型设计模式,确保一个类在 同一个
JVM中仅存在一个实例,并提供全局访问点。
1、作用和使用场景
1)作用
- 节省系统资源:避免频繁创建和销毁重量级对象(如数据库连接池、日志管理器等)。
- 保证全局唯一性:对核心控制类(如交易引擎、配置中心)必须全局唯一,防止逻辑混乱(例如多个“司令员”同时指挥)。
2)使用场景
| 场景 | 问题 | 单例如何解决 |
|---|---|---|
| 文件写入 | 多线程并发写导致混乱 | 提供唯一写入入口,内部同步 |
| 连接池初始化 | 重复创建耗尽资源 | 全局唯一实例,一次初始化 |
| 配置共享 | 模块间无法直接传参 | 全局可访问的配置容器 |
a、控制对共享资源的并发访问(如文件写入、缓存管理)
场景举例:日志记录器(Logger)
- 多个线程同时需要写日志到同一个文件。
- 如果每个线程都创建自己的
FileWriter,会导致:- 文件被多个流同时打开,可能损坏;
- 日志内容交错混乱;
- 资源浪费(大量文件句柄)。
public class Logger {
private static final Logger INSTANCE = new Logger();
private FileWriter writer;
private Logger() {
try {
writer = new FileWriter("app.log", true); // 追加模式
} catch (IOException e) { /* ... */ }
}
public static Logger getInstance() {
return INSTANCE;
}
public synchronized void log(String message) {
try {
writer.write(LocalDateTime.now() + ": " + message + "\n");
writer.flush();
} catch (IOException e) { /* ... */ }
}
}
b、节约内存和 CPU 开销,避免重复初始化
场景举例:数据库连接池(DataSource)
- 初始化连接池(如 HikariCP、Druid)开销大:需建立多个物理连接、加载驱动、验证配置等。
- 若每个 DAO 类都新建一个连接池,系统将迅速耗尽内存和数据库连接数。
public class DatabasePool {
private static volatile DatabasePool instance;
private HikariDataSource dataSource;
private DatabasePool() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("root");
config.setPassword("123456");
config.setMaximumPoolSize(20);
this.dataSource = new HikariDataSource(config);
}
public static DatabasePool getInstance() {
if (instance == null) {
synchronized (DatabasePool.class) {
if (instance == null) {
instance = new DatabasePool();
}
}
}
return instance;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
c、在无直接依赖的情况下,实现跨模块/线程的数据共享
场景举例:应用配置中心(AppConfig)
- 系统启动时从
application.properties加载配置(如 API 密钥、超时时间、开关标志)。 - 多个服务类(UserService、PaymentService、NotificationService)都需要读取这些配置,但彼此无调用关系。
public class AppConfig {
private static class Holder {
static final AppConfig INSTANCE = new AppConfig();
}
private Properties props = new Properties();
private AppConfig() {
try (InputStream in = getClass().getResourceAsStream("/application.properties")) {
props.load(in);
} catch (IOException e) {
throw new RuntimeException("Failed to load config", e);
}
}
public static AppConfig getInstance() {
return Holder.INSTANCE;
}
public String getProperty(String key) {
return props.getProperty(key);
}
// 示例:获取支付超时时间
public int getPaymentTimeoutMs() {
return Integer.parseInt(getProperty("payment.timeout.ms"));
}
}
3)不是所有地方都“必须”用 final
| 场景 | 是否用 final |
理由 |
|---|---|---|
| 单例实例(饿汉/静态内部类/枚举) | 是 | 安全、清晰、可优化 |
| 单例类本身 | 是 | 防止继承破坏封装 |
| 配置类字段(如数据库 URL) | 是 | 初始化后不应改变 |
| 方法参数(如工具方法) | 推荐 | 防止意外修改,提升可读性 |
懒汉式 DCL 的 instance 字段 |
否 | 需要运行时赋值 |
| 会变化的状态字段 | 否 | 逻辑上需要修改 |
2、单例模式的 7 种写法
| 实现方式 | 线程安全 | 延迟加载 | 防反射 | 防反序列化 | 推荐度 |
|---|---|---|---|---|---|
| 懒汉(无锁) | ❌ | ✅ | ❌ | ❌ | ⭐ |
懒汉(synchronized) |
✅ | ✅ | ❌ | ❌ | ⭐⭐ |
| 饿汉式 | ✅ | ❌ | ❌ | ❌ | ⭐⭐ |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | ⭐⭐⭐⭐ |
| 枚举 | ✅ | ✅* | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
1)懒汉式(线程不安全)
- 问题:多线程环境下不安全。
- 优点:延迟加载(
Lazy Loading)。
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); //// 多线程下可能创建多个实例
}
return instance;
}
}
2)懒汉式(线程安全,方法加锁)
- 缺点:每次调用都加锁,性能差(99% 的情况无需同步)。
synchronized关键字锁住的是这个类对象,,在性能上会下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进
- 优点:线程安全 + 延迟加载。
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3)饿汉式(静态变量)
- 优点:线程安全(类加载时初始化),实现简单。
- 缺点:非延迟加载,即使未使用也会占用内存。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
4)饿汉式(静态代码块)
表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化
instance。
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
5)静态内部类(推荐)
优点:这是懒汉与饿汉的最佳结合。
- 利用类加载机制保证线程安全,保证初始化
instance时只有一个线; - 真正实现延迟加载(只有调用
getInstance()时才加载Holder类); - 无性能损耗。
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
}
6)枚举实现(最推荐)
优势:
- 天然线程安全(
JVM保证枚举实例唯一); - 自动防止反射攻击(
Constructor.newInstance()对枚举抛出异常); - 自动防止反序列化破坏(反序列化时通过
name查找已有实例,而非新建); - 代码简洁,无额外开销。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
7)双重检查锁(需谨慎使用)
关键点:
- 两次判空:第一次避免不必要的同步,第二次防止多线程重复创建;
volatile关键字:禁止指令重排序,确保其他线程看到的是完全初始化的对象。
public class SingletonFinal {
/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
private volatile static SingletonFinal instance = null;
/* 私有构造方法,防止被实例化 */
private SingletonFinal() {
}
/* 1、静态工程方法,创建实例 */
public static SingletonFinal getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new SingletonFinal();
}
}
}
3、单例模式的安全隐患
1)反射攻击
public class Singleton {
/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
private volatile static Singleton instance = null;
/* 私有构造方法,防止被实例化 */
private SingletonFinal() {
}
/* 1、静态工程方法,创建实例 */
public static Singleton getInstance() {
if (instance == null) {
syncInit();
}
return instance;
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new Singleton();
}
}
}
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton); //false
}
// 这两个实例不是同一个,这就违背了单例模式的原则了。
2)反序列化攻击
public class Singleton implements Serializable {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
System.out.println(instance == newInstance); //false
// 这两个实例不是同一个,这就违背了单例模式的原则了。
}
}
4、单例模式缺点
1)不支持带参构造
无法在获取实例时传入参数(如连接池大小)。
应对策略:
- 参数通过配置文件、系统属性或静态 setter 注入;
- 或考虑工厂模式 + 单例注册表。
2)扩展性差
若未来需要多个实例(如多数据源),需重构代码。
建议:评估是否真的需要“全局唯一”,避免过度使用单例。
5、枚举类型实现的单例模式是最佳的方式
枚举方式实现的单例模式不仅能避免多线程同步的问题,也可以防止反序列化和反射的破坏。
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
1、JVM 级别的线程安全
反编译的代码中可以发现枚举中的各个枚举项都是通过 static 代码块来定义和初始化的,他们会在类被加载时完成初始化,而 Java 的类加载由 JVM 保证线程安全。
2、防止反序列化的破坏
-
在序列化时,只是将枚举对象的
name属性输出到结果中, -
在反序列化时通过
java.lang.Enum的valueOf方法根据名字查找对象,而不是新建一个新的对象,所以防止了反序列化对单例的破坏。
3、防止反射的破坏
反射在通过newInstance创建对象时会检查这个类是否是枚举类,如果是枚举类就会throw new IllegalArgumentException("Cannot reflectively create enum objects");,如下是源码java.lang.reflect.Constructor#newInstance:


