设计模式:单例模式

📅 2026/6/17 17:51:13
设计模式:单例模式
设计模式单例模式目录单例模式是什么为什么需要单例五种写法全览为什么 DCL 必须加 volatile实际应用场景小结单例模式是什么单例模式是后端面试中的一个热点一个类在整个应用中只能有一个实例并提供一个全局访问点。单例模式要做的就是这件事用代码保证某个对象全局唯一谁来拿都是同一个。为什么需要单例假设你写了一个数据库连接池管理器每个模块各自new了一个实例// 订单模块ConnectionPoolpoolAnewConnectionPool();// 用户模块ConnectionPoolpoolBnewConnectionPool();// 支付模块ConnectionPoolpoolCnewConnectionPool();三个模块各自初始化了一个连接池数据库连接数直接翻了三倍。更严重的是它们各自维护自己的连接状态互相不知道对方用了多少连接连接泄漏了也没法统一排查。多个实例意味着资源被浪费、状态不一致、管理失控。连接池、线程池、配置中心、日志记录器这些东西天然就应该是全局唯一的。单例模式就是用代码强制保证这种唯一性。五种写法全览写法一饿汉式最直白的写法类加载的时候就创建实例不管你用不用。publicclassSingleton{// 类加载时就创建实例privatestaticfinalSingletonINSTANCEnewSingleton();// 私有构造禁止外部 newprivateSingleton(){}// 全局访问点publicstaticSingletongetInstance(){returnINSTANCE;}}如何做到线程安全 JVM 的类加载机制保证了static字段的初始化只会执行一次这个过程是线程安全的。我们在使用的时候就不需要加锁因为JVM 帮我们兜底了。缺点也很明显不管你用不用类一加载实例就创建好了。如果这个对象很重比如初始化时要加载大量配置文件而应用启动后可能很长时间都用不到它那这块资源就会被一直白白占着。写法二懒汉式为了解决用不到就别创建的问题延迟到第一次调用getInstance()时再创建publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){instancenewSingleton();}returninstance;}}看起来没问题但多线程下会出事。两个线程同时调getInstance()都判断instance null于是各自创建了一个实例单例就被打破了。写法三懒汉式 synchronized加锁让同一时刻只有一个线程能进入创建逻辑publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instancenull){instancenewSingleton();}returninstance;}}线程安全了但性能差。synchronized加在方法上意味着每次调用getInstance()都要拿锁哪怕实例早就创建好了读操作也要排队。在高并发场景下这里会成为瓶颈。写法四双重检查锁DCL既然只有第一次创建时需要加锁后续读取不需要那我们可以把锁的粒度缩小publicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){// 第一次检查不加锁synchronized(Singleton.class){if(instancenull){// 第二次检查加锁后再确认instancenewSingleton();}}}returninstance;}}第一个if是为了性能实例已经创建了直接返回不用排队抢锁。第二个if是为了安全可能有两个线程同时通过了第一个if进入synchronized块后必须再确认一次避免重复创建。关键点instance必须用volatile修饰。这一点我们在后文中单独拎出来讲。写法五静态内部类这是兼顾延迟加载和线程安全的最优写法之一publicclassSingleton{privateSingleton(){}// 静态内部类只有在被引用时才会加载privatestaticclassHolder{privatestaticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}}原理Java 的内部类只有在真正被使用时才会被类加载器加载。当getInstance()第一次被调用时Holder类才会初始化INSTANCE才会被创建。在此之前Holder类根本不存在于内存中。线程安全同样由 JVM 的类加载机制保证不需要synchronized不需要volatile代码简洁性能最优。写法六枚举《Effective Java》作者 Joshua Bloch 最推荐的写法publicenumSingleton{INSTANCE;publicvoiddoSomething(){System.out.println(执行业务逻辑);}}就这么多代码。枚举天然是单例的JVM 保证枚举实例的唯一性。更重要的是枚举能防御反射和序列化攻击这是其他写法都做不到的。为什么 DCL 必须加 volatile问题出在instance new Singleton()这一行。maybe你以为它是一个原子操作实际上 JVM 执行它分了三步1. 分配内存空间 2. 初始化对象执行构造方法 3. 将 instance 指向分配的内存如果没有volatileJVM 可能会进行指令重排序把步骤 2 和步骤 3 调换顺序1. 分配内存空间 2. 将 instance 指向分配的内存 ← 先指过去了 3. 初始化对象 ← 但对象还没初始化完这会导致什么问题线程 A 执行到第 2 步instance已经不是null了但对象还没初始化完成。此时线程 B 调用getInstance()第一个if判断instance null为false直接返回了一个尚未初始化完成的对象。用这个对象去调方法轻则数据错误重则直接NullPointerException。volatile用来禁止指令重排序。加了volatile之后JVM 会保证instance new Singleton()的三步严格按顺序执行不会出现指针先到位、对象还没好的情况。实际应用场景JDK 中的单例Runtime.getRuntime()就是一个经典的饿汉式单例。每个 Java 应用只有一个 Runtime 实例通过它获取 JVM 内存信息、执行系统命令。Desktop.getDesktop()也是单例用于打开浏览器、邮件客户端等桌面操作。Spring BeanSpring 容器中的 Bean 默认就是单例模式。你在代码里注入的Autowired UserService userService整个应用里拿到的都是同一个实例。Spring 通过 IoC 容器管理 Bean 的生命周期本质上就是一个单例注册表。你可以通过Scope(prototype)改成多例但绝大多数 Service、DAO 都是单例因为它们本身无状态单例既节省资源又不影响并发。数据库连接池Druid 等连接池在应用中通常只有一个实例全局共享统一管理连接的创建、回收、监控。小结单例模式的核心保证一个类全局只有一个实例。但围绕这个简单的目标衍生出了线程安全、延迟加载、指令重排序等一系列问题。每种写法都是在这些维度之间做取舍没有完美的方案只有最适合你场景的方案。