34_Java设计模式之单例模式

Java设计模式之单例模式

文章目录

前言

**单例模式(Singleton Pattern)**是创建型设计模式中最基础也最常用的模式之一。它确保一个类在整个JVM中只有一个实例,并提供一个全局访问点。典型场景包括配置管理器、数据库连接池、Spring容器中的Bean等。本文将从饿汉式到枚举式,逐一分析每种实现方式的原理与适用场景。

面试视角:单例模式几乎是所有Java面试的必考设计模式,面试官常从最基础的"写一个单例"开始,然后逐步追问线程安全、双重检查锁定的volatile作用、序列化破坏单例、反射破坏单例、枚举单例的原理等。本文覆盖了这些层层递进的考点,建议你不仅看代码,更要理解每种实现方式的设计思路和适用边界。

一、饿汉式(Eager Initialization)

类加载时就创建实例,简单可靠,天生线程安全:

java 复制代码
public class EagerSingleton {
    // 类加载时立即实例化
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // 私有构造器,防止外部new
    private EagerSingleton() {
        // 防止反射破坏
        if (INSTANCE != null) {
            throw new RuntimeException("单例已存在,不允许反射创建");
        }
    }

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

优点 :实现简单,线程安全。

缺点:无论是否使用都会创建实例,可能浪费内存。如果实例化依赖某些运行时参数则无法使用。适用于对象轻量且一定会被使用的场景。

实际项目中的考量:饿汉式的"浪费内存"在大多数场景下被夸大了------一个配置管理器或连接池对象通常只占几KB,在几个GB的JVM堆内存中微乎其微。所以如果你不需要延迟加载,饿汉式是完全可接受的选择。Spring容器中的单例Bean本质上就是饿汉式(默认在启动时完成实例化),这也是Spring团队认为"启动时就发现问题"比"运行时才发现问题"更重要的设计哲学。

二、懒汉式(Lazy Initialization)

延迟加载,首次调用时才创建:

java 复制代码
// 线程不安全版本
public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

上述代码在多线程环境下可能创建多个实例。修复方法------加synchronized

java 复制代码
// 同步方法版本
public class SyncLazySingleton {
    private static SyncLazySingleton instance;

    private SyncLazySingleton() {}

    public static synchronized SyncLazySingleton getInstance() {
        if (instance == null) {
            instance = new SyncLazySingleton();
        }
        return instance;
    }
}

虽然线程安全了,但每次调用getInstance()都要加锁,性能较差。

三、双重检查锁定(DCL - Double-Checked Locking)

在保证线程安全的同时,尽量减少同步开销:

java 复制代码
public class DCLSingleton {
    // volatile 防止指令重排序
    private static volatile DCLSingleton instance;

    private DCLSingleton() {
        if (instance != null) {
            throw new RuntimeException("禁止反射破坏单例");
        }
    }

    public static DCLSingleton getInstance() {
        if (instance == null) {                    // 第一重检查
            synchronized (DCLSingleton.class) {
                if (instance == null) {            // 第二重检查
                    instance = new DCLSingleton(); // 非原子操作
                }
            }
        }
        return instance;
    }
}

volatile的作用instance = new DCLSingleton()不是原子操作,分为三步:①分配内存 → ②初始化对象 → ③将引用指向内存。JIT可能将步骤②和③重排序。线程A执行到③但未完成②时,线程B在第一重检查发现instance != null,拿到一个未初始化完成的对象。volatile禁止了这种指令重排序。

这里需要强调一个细节:volatile对指令重排序的禁止能力是在Java 5之后才修复的。如果你还在维护Java 1.4甚至更早的代码,DCL是不可靠的(尽管这种情况现在极为罕见)。这也是为什么有些老项目中会看到各种"奇技淫巧"的单例实现------其实只是那个年代JMM不完善的产物。

四、静态内部类(推荐)

利用类加载机制保证线程安全,同时实现延迟加载:

java 复制代码
public class HolderSingleton {
    private HolderSingleton() {}

    // 静态内部类,只有在被调用时才会加载
    private static class Holder {
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }

    public static HolderSingleton getInstance() {
        return Holder.INSTANCE;  // 触发Holder类加载
    }
}

JVM保证类的加载是线程安全的,Holder类只在getInstance()首次调用时加载并初始化INSTANCE。这种方式代码简洁、线程安全、延迟加载,是最推荐的实现方式之一

尽管静态内部类很优雅,但它有一个微小缺点:无法在构造时传入参数。如果单例的创建依赖外部配置(如从配置文件读取连接信息),静态内部类的方式就不太方便了------这时DCL或枚举配合初始化方法会更灵活。在Spring中,可以通过@Value注入配置后,在@PostConstruct方法中初始化单例所需的资源。

五、枚举单例(最安全)

利用枚举类型天然单例、防反射、防序列化的特性:

java 复制代码
public enum EnumSingleton {
    INSTANCE;

    // 可以在枚举中添加方法和字段
    private String config;

    public void setConfig(String config) {
        this.config = config;
    }

    public String getConfig() {
        return config;
    }

    public void doSomething() {
        System.out.println("执行单例方法: " + config);
    }
}

// 使用
EnumSingleton.INSTANCE.setConfig("数据库连接串");
EnumSingleton.INSTANCE.doSomething();

枚举单例的优势

  • JVM层面保证只有一个实例
  • 天然防止反射攻击(Constructor.newInstance()对枚举抛出异常)
  • 天然防止序列化破坏(反序列化返回同一个实例)
  • 代码极简

《Effective Java》作者Joshua Bloch也推荐这种方式。唯一的"缺点"是无法延迟加载(枚举类加载时就初始化),但这在大多数场景下不是问题。

深入理解 :枚举单例为什么能防反射?因为在Constructor.newInstance()源码中显式判断了如果类是Enum类型,直接抛出IllegalArgumentException: Cannot reflectively create enum objects。为什么能防序列化?因为Java序列化对枚举有特殊处理------反序列化时不是创建新对象,而是通过Enum.valueOf()方法按名称查找已存在的枚举实例。这两点不是巧合,而是JVM规范特意为枚举设计的保障。

六、序列化安全问题

普通单例类在反序列化时会创建新对象,破坏单例。解决方式------添加readResolve()方法:

java 复制代码
import java.io.Serializable;

public class SafeSerializableSingleton implements Serializable {
    private static final SafeSerializableSingleton INSTANCE
            = new SafeSerializableSingleton();

    private SafeSerializableSingleton() {}

    public static SafeSerializableSingleton getInstance() {
        return INSTANCE;
    }

    // 反序列化时返回已有实例,而非创建新对象
    private Object readResolve() {
        return INSTANCE;
    }
}

总结

实现方式 线程安全 延迟加载 防反射 防序列化 推荐度
饿汉式 可防 需处理 ★★★
同步懒汉式 需处理 需处理 ★★
DCL 需处理 需处理 ★★★★
静态内部类 需处理 需处理 ★★★★★
枚举 天然 天然 ★★★★★

实际开发中,推荐优先使用枚举单例静态内部类方式。Spring框架中单例Bean的实现本质上类似于饿汉式,在容器启动时就完成实例化。

最后的忠告 :单例模式虽好,但不要滥用。它本质上是全局状态,会带来隐式的耦合,使单元测试变得困难(需要使用反射或Mockito的@InjectMocks等黑魔法来替换单例)。如果你的单例越来越多,可能意味着设计上需要重新审视------是否可以用依赖注入来代替全局访问。在Spring项目中,单例Bean是由容器管理的,你不需要手写单例模式,只需将类标注为@Component并确保它是无状态的即可。

✅ 亮点总结

  • 五种实现方式(饿汉式、同步懒汉式、DCL、静态内部类、枚举)的线程安全、延迟加载、防反射特性全对比
  • DCL的双重检查 + volatile 禁止指令重排,Java 5+内存模型的经典面试考点
  • 静态内部类利用JVM类加载机制保证线程安全且延迟加载,实现简洁优雅
  • 枚举单例天然防反射和防序列化------Constructor.newInstance() 抛异常,readResolve() 无需手写
  • 序列化破坏单例的问题与 readResolve() 解决方案,Jackson反序列化同理

适用场景

  • Spring Bean的生命周期管理------理解容器启动时如何创建和管理单例Bean
  • 数据库连接池------整个应用共享一个连接池实例,避免反复创建销毁
  • 配置管理器------加载一次配置文件,全局共享配置对象

扩展方向

  • Spring单例 vs 设计模式单例:了解Spring IoC容器的单例Bean作用域与单例模式的差异(推荐阅读下一篇:Java设计模式之工厂模式)
  • CAS实现无锁单例 :用 AtomicReference + CAS 实现高性能非阻塞单例
  • 分布式环境下的单例:基于Redis分布式锁或Zookeeper选主实现跨JVM的单例
相关推荐
摇滚侠1 小时前
MyBatis 入门到项目实战 IDEA 配置模板 20-22
java·intellij-idea·mybatis
技术小结-李爽1 小时前
【工具】Maven二进制包目录结构说明
java·maven
zyl837211 小时前
前后端高并发解决方案
java·redis
Doker 多克1 小时前
Spring AI Alibaba—快速构建ReactAgent
java·开发语言·前端·ai编程
i220818 Faiz Ul2 小时前
药店管理|基于springboot + vue药店管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·美食分享系统
满怀冰雪2 小时前
第15篇-链表基础-反转链表-合并链表与快慢指针
java·算法·链表
番茄去哪了2 小时前
RabbitMQ
java·rabbitmq·java-rabbitmq
西凉的悲伤2 小时前
redis-windows 安装 redis 到 windows 电脑
java·windows·redis·redis-windows
starsky762382 小时前
NIO与BIO的区别
java·服务器·nio