零基础设计模式——创建型模式 - 单例模式
第二部分:创建型模式 - 单例模式 (Singleton Pattern)
欢迎来到创建型模式的第一站——单例模式!这是最简单也最常用的设计模式之一。
- 核心思想:关注对象的创建过程,将对象的创建与使用分离,降低系统的耦合度。
单例模式 (Singleton Pattern)
“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
想象一下,你们班只有一个班长,或者你们学校只有一个校长。无论什么时候提到“我们班班长”或“我们学校校长”,指的都是同一个人。这个人就是“单例”。
1. 目的 (Intent)
单例模式的核心目的就两个:
- 保证唯一实例:确保一个类在任何情况下都只有一个对象实例存在。就像一个国家只有一个现任总统。
- 提供全局访问点:提供一个统一的方法,让程序的任何地方都可以方便地获取到这个唯一的实例。就像你可以通过“总统办公室”这个统一的渠道去联系总统。
2. 生活中的例子 (Real-world Analogy)
- 身份证号码:每个人在中国只有一个唯一的身份证号码。这个号码全局唯一,代表了你这个人。
- 电脑的回收站/垃圾桶:通常一台电脑只有一个回收站。你删除的文件都会进入这个唯一的回收站。
- 打印机后台服务 (Print Spooler):当你点击打印时,打印任务会进入一个打印队列,由一个后台服务程序统一管理。这个服务程序通常是单例的,确保所有打印任务有序进行,不会因为多个程序同时直接控制打印机而造成混乱。
- 皇帝:在一个封建王朝,通常只有一个皇帝。他是国家的最高统治者,独一无二。
- 应用程序的配置管理器:一个应用程序通常只需要一份配置信息。这个配置管理器对象就可以设计成单例,确保所有模块读取到的配置都是一致的。
- 日志对象 (Logger):在一个应用中,通常也只需要一个日志记录器,将所有日志信息汇总到同一个地方。
3. 适用场景 (When to Use)
什么时候应该考虑使用单例模式呢?
- 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
- 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时(虽然这点在实践中较少直接通过单例实现,更多的是结合其他模式)。
- 资源共享与控制:比如数据库连接池、线程池。这些资源通常是有限的,通过单例模式可以统一管理和分配,避免资源浪费或冲突。
- 全局状态管理:比如应用的配置信息、全局计数器等。
- 工具类:如果一个工具类没有状态(即其方法不依赖于实例变量),并且需要频繁创建和销毁其实例会造成不必要的开销,可以考虑将其设计为单例(尽管对于无状态工具类,静态方法通常更简单)。
4. 优缺点 (Pros and Cons)
优点:
- 保证实例唯一:这是核心优点,确保了对象在内存中只有一个副本,节约了系统资源。
- 全局访问:提供了一个方便的全局访问点。
- 延迟实例化 (Lazy Initialization):可以在第一次被请求时才创建实例,如果未使用,则不创建,节约资源(特指懒汉式实现)。
缺点:
- 违反单一职责原则:单例类既要负责自身的业务逻辑,又要负责保证自身实例的唯一性,承担了过多的职责。
- 扩展困难:通常单例类的构造函数是私有的,这使得它很难被继承和扩展。如果想创建子类实例,会比较麻烦。
- 对测试不友好:如果单例依赖于外部资源(如数据库、文件),那么在单元测试时很难模拟或替换这个单例,导致测试困难。
- 可能被滥用:由于其简单易用,容易被滥用,导致代码中出现过多的全局状态,增加模块间的耦合度。
- 并发问题:在多线程环境下,懒汉式单例的创建需要特别注意线程安全问题,否则可能创建出多个实例。
5. 实现方式 (Implementations)
单例模式的实现方式有很多种,主要分为“饿汉式”和“懒汉式”。
饿汉式 (Eager Initialization)
特点:类加载时就立即创建实例,不管你用不用,它都在那里。线程安全。
生活例子:你开了一家餐馆,不管有没有客人来,你每天早上开门前就把招牌菜“红烧肉”做好一份备着。客人点餐时直接端上去。
Golang 实现 (饿汉式)
package singletonimport "fmt"// ConfigManager 配置管理器 (饿汉式)
type ConfigManager struct {settings map[string]string
}var instance = &ConfigManager{ // 类加载时就初始化settings: make(map[string]string),
}// GetInstance 获取唯一实例
func GetInstance() *ConfigManager {return instance
}func (cm *ConfigManager) Set(key, value string) {cm.settings[key] = value
}func (cm *ConfigManager) Get(key string) (string, bool) {val, ok := cm.settings[key]return val, ok
}// main.go (示例用法)
/*
package mainimport ("fmt""./singleton" // 假设 singleton 包在当前目录下
)func main() {cm1 := singleton.GetInstance()cm1.Set("appName", "My Awesome App")cm2 := singleton.GetInstance()appName, _ := cm2.Get("appName")fmt.Println("App Name:", appName) // 输出: App Name: My Awesome Appfmt.Println(cm1 == cm2) // 输出: true
}
*/
Java 实现 (饿汉式)
package com.example.singleton;import java.util.HashMap;
import java.util.Map;// ConfigManager 配置管理器 (饿汉式)
public class ConfigManagerEager {// 1. 私有静态实例,在类加载时就创建private static final ConfigManagerEager instance = new ConfigManagerEager();private Map<String, String> settings;// 2. 私有构造方法,防止外部通过 new 创建实例private ConfigManagerEager() {settings = new HashMap<>();System.out.println("饿汉式 ConfigManager 已创建.");}// 3. 公有静态方法,返回唯一实例public static ConfigManagerEager getInstance() {return instance;}public void set(String key, String value) {settings.put(key, value);}public String get(String key) {return settings.get(key);}// Main.java (示例用法)/*public static void main(String[] args) {ConfigManagerEager cm1 = ConfigManagerEager.getInstance();cm1.set("databaseUrl", "jdbc:mysql://localhost:3306/mydb");ConfigManagerEager cm2 = ConfigManagerEager.getInstance();String dbUrl = cm2.get("databaseUrl");System.out.println("Database URL: " + dbUrl); // 输出: Database URL: jdbc:mysql://localhost:3306/mydbSystem.out.println(cm1 == cm2); // 输出: true}*/
}
懒汉式 (Lazy Initialization)
特点:第一次调用获取实例的方法时才创建实例。如果一直不调用,就不创建。需要处理多线程安全问题。
生活例子:你开了一家奶茶店。只有当客人点了“珍珠奶茶”时,你才开始现场制作这杯奶茶。没客人点,你就不做。
Golang 实现 (懒汉式 - 带锁线程安全)
package singletonimport ("fmt""sync"
)// Logger 日志记录器 (懒汉式 - 线程安全)
type Logger struct {name string
}var (lazyInstance *Loggeronce sync.Once // sync.Once 保证初始化代码只执行一次
)// GetLazyInstance 获取唯一实例 (懒汉式)
func GetLazyInstance() *Logger {once.Do(func() { // Do 方法中的函数只会被执行一次lazyInstance = &Logger{name: "GlobalLogger"}fmt.Println("懒汉式 Logger 已创建.")})return lazyInstance
}func (l *Logger) Log(message string) {fmt.Printf("[%s]: %s\n", l.name, message)
}// main.go (示例用法)
/*
package mainimport ("fmt""./singleton""sync"
)func main() {var wg sync.WaitGroupfor i := 0; i < 5; i++ {wg.Add(1)go func(id int) {defer wg.Done()logger := singleton.GetLazyInstance()logger.Log(fmt.Sprintf("Goroutine %d is logging", id))}(i)}wg.Wait()logger1 := singleton.GetLazyInstance()logger2 := singleton.GetLazyInstance()fmt.Println(logger1 == logger2) // 输出: true (如果之前有goroutine执行过,则不会再打印创建信息)
}
*/
说明:在Go中,sync.Once
是实现线程安全的懒汉式单例的最佳方式。它能确保即使在多个goroutine并发调用GetLazyInstance
时,初始化代码(创建实例的那部分)也只会被执行一次。
Java 实现 (懒汉式 - 双重校验锁 DCL - 线程安全)
package com.example.singleton;// Logger 日志记录器 (懒汉式 - 双重校验锁 DCL)
public class LoggerLazyDCL {// 1. 私有静态实例,volatile 保证可见性和禁止指令重排private static volatile LoggerLazyDCL instance;private String name;// 2. 私有构造方法private LoggerLazyDCL(String name) {this.name = name;System.out.println("懒汉式 Logger (DCL) 已创建: " + name);}// 3. 公有静态方法,返回唯一实例 (双重校验锁)public static LoggerLazyDCL getInstance(String name) {if (instance == null) { // 第一次检查,不加锁,提高性能synchronized (LoggerLazyDCL.class) { // 加类锁if (instance == null) { // 第二次检查,防止多个线程同时通过第一次检查instance = new LoggerLazyDCL(name);}}}return instance;}public void log(String message) {System.out.println("[" + this.name + "]: " + message);}// Main.java (示例用法)/*public static void main(String[] args) {Thread t1 = new Thread(() -> {LoggerLazyDCL logger = LoggerLazyDCL.getInstance("AppLogger");logger.log("Thread 1 logging");});Thread t2 = new Thread(() -> {LoggerLazyDCL logger = LoggerLazyDCL.getInstance("AppLogger"); // 即使传入不同name,也只会创建一次logger.log("Thread 2 logging");});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}LoggerLazyDCL logger1 = LoggerLazyDCL.getInstance("AppLogger");LoggerLazyDCL logger2 = LoggerLazyDCL.getInstance("AnotherLoggerName");System.out.println(logger1 == logger2); // 输出: true}*/
}
Java DCL (Double-Checked Locking) 注意事项:
instance
变量必须用volatile
修饰。这是因为instance = new LoggerLazyDCL(name);
这行代码并非原子操作,它大致可以分为三步:- 为
instance
分配内存空间。 - 初始化
LoggerLazyDCL
对象。 - 将
instance
引用指向分配的内存地址。
JVM 可能会进行指令重排序,导致步骤 2 和 3 的顺序颠倒。如果一个线程执行了 1 和 3,但还没执行 2,此时另一个线程进来,发现instance
不为null
,就会直接返回一个未完全初始化的对象,从而引发问题。volatile
可以禁止这种指令重排,并保证多线程间的可见性。
- 为
其他实现方式 (Java 特有)
-
静态内部类 (Static Inner Class):利用了类加载机制来保证线程安全和延迟加载,是Java中推荐的懒汉式实现。
package com.example.singleton;public class StaticInnerClassSingleton {private StaticInnerClassSingleton() {System.out.println("静态内部类单例已创建.");}private static class SingletonHolder {private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();}public static StaticInnerClassSingleton getInstance() {return SingletonHolder.INSTANCE;}public void showMessage(){System.out.println("Hello from Static Inner Class Singleton!");}// Main.java (示例用法)/*public static void main(String[] args) {StaticInnerClassSingleton s1 = StaticInnerClassSingleton.getInstance();StaticInnerClassSingleton s2 = StaticInnerClassSingleton.getInstance();s1.showMessage();System.out.println(s1 == s2); // true}*/ }
原理:当
StaticInnerClassSingleton
类加载时,静态内部类SingletonHolder
不会立即被加载。只有当第一次调用getInstance()
方法时,才会触发SingletonHolder
的加载,此时静态变量INSTANCE
才会被初始化,并且由JVM保证线程安全。 -
枚举 (Enum):最简洁、最安全(天然防止反射和反序列化破坏单例)的实现方式,由《Effective Java》作者 Joshua Bloch 推荐。
package com.example.singleton;public enum EnumSingleton {INSTANCE;private EnumSingleton() {System.out.println("枚举单例已创建.");}public void showMessage(){System.out.println("Hello from Enum Singleton!");}// Main.java (示例用法)/*public static void main(String[] args) {EnumSingleton s1 = EnumSingleton.INSTANCE;EnumSingleton s2 = EnumSingleton.INSTANCE;s1.showMessage();System.out.println(s1 == s2); // true}*/ }
6. 总结
单例模式是一种简单但功能强大的模式,用于确保类只有一个实例并提供全局访问点。选择哪种实现方式取决于具体需求:
- 饿汉式:简单,线程安全,但可能造成资源浪费(如果实例一直不用)。
- 懒汉式 (Go
sync.Once
):线程安全,延迟加载,Go语言推荐。 - 懒汉式 (Java DCL):线程安全,延迟加载,但实现略复杂,需要注意
volatile
。 - 静态内部类 (Java):线程安全,延迟加载,实现优雅,Java中常用。
- 枚举 (Java):最简洁、最安全的实现方式,能防止反射和反序列化攻击,Java中极力推荐。
理解单例模式是学习其他设计模式的基础。记住它的核心思想:唯一和全局访问。