单例模式完整教程

设计模式中最基础也最常用的创建型模式,保证一个类在整个应用中只有一个实例

一、模式详细介绍

基本定义

单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类在整个应用程序的生命周期中 只能创建一个实例,并且提供一个全局统一的访问点来获取这个实例。

核心思想

通过私有化构造方法,阻止外部直接实例化类;然后通过一个静态方法来控制类的实例化过程, 确保无论调用多少次这个方法,都只会返回同一个对象实例。

核心特点

✅ 唯一性

一个类在整个应用中只能有一个实例

✅ 全局访问性

提供一个全局统一的访问点

✅ 延迟初始化

可以在第一次使用时才创建实例

✅ 线程安全

多线程环境下也能保证唯一性

二、单例模式的核心优势

1 精确控制资源访问

对于需要独占访问的资源(如数据库连接池、文件系统、打印机等),单例模式可以确保只有一个实例在操作这些资源, 避免了多个实例同时访问资源导致的冲突、数据不一致和资源竞争问题。

2 显著节省系统资源

创建和销毁对象是有性能开销的,尤其是对于那些创建成本高的对象(如数据库连接、网络连接、配置管理器等)。 单例模式避免了频繁创建和销毁对象的开销,只创建一次实例并重复使用,可以显著提高系统性能。

3 全局统一的状态管理

当需要在应用的多个部分之间共享状态时,单例模式提供了一个全局统一的状态存储和访问点。 所有对状态的修改和读取都通过同一个实例进行,保证了状态的一致性和可预测性。

4 简化代码结构

单例模式将对象的创建和使用分离,客户端不需要关心对象的创建细节,只需要通过统一的接口获取实例即可。 这简化了客户端代码,减少了重复的对象创建逻辑,提高了代码的可维护性。

5 灵活的生命周期控制

单例模式允许开发者精确控制实例的创建时机和生命周期。可以实现延迟加载(懒加载), 只有在真正需要时才创建实例,避免了不必要的资源占用;也可以实现饿汉式加载,在应用启动时就完成初始化。

实际应用场景:数据库连接池、日志记录器、配置管理器、应用上下文、线程池、缓存管理器、打印机管理器等。

三、示例说明

我们以一个数据库连接管理器为例来演示单例模式的实际应用。

在一个典型的Web应用中,数据库连接是一种非常宝贵的资源。如果每次数据库操作都创建一个新的连接池, 不仅会造成巨大的性能开销,还可能导致数据库连接数耗尽,引发系统崩溃。

使用单例模式,我们可以确保整个应用程序中只有一个DatabaseManager实例, 所有对数据库的操作都通过这个唯一的实例进行。这个实例负责管理数据库连接池, 统一分配和回收连接,从而实现资源的高效利用。

这样不仅可以节省系统资源,还可以统一管理数据库连接的生命周期,避免连接泄漏, 提高系统的稳定性和可维护性。

四、UML类图

classDiagram class Singleton { -_instance Singleton -_lock Lock -__init__() +getInstance() Singleton +businessMethod() void } note "构造方法是私有的,\n只能通过getInstance()方法获取实例"

注:__init__方法为私有方法,防止外部直接实例化;_lock用于保证线程安全

五、Python核心实现方式

单例模式最经典的两种实现方式是饿汉式懒汉式,它们的主要区别在于实例创建的时机不同。

5.1 饿汉式(Eager Initialization)

最简单

核心特点:类加载时就立即创建实例,不管后续是否会使用。

优缺点:✅ 线程安全,实现简单;❌ 无法实现延迟加载,如果实例从未被使用会造成资源浪费。

class DatabaseManager:
    # 类加载时就创建唯一实例
    _instance = None
    
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self, connection_string):
        # 初始化数据库连接池(只会执行一次)
        if not hasattr(self, 'initialized'):
            self.connection_string = connection_string
            self.pool = self._create_connection_pool()
            print(f"数据库连接池初始化完成: {connection_string}")
            self.initialized = True
    
    def _create_connection_pool(self):
        # 创建数据库连接池的实际逻辑
        return {"connections": 10, "max_connections": 20}
    
    def get_connection(self):
        # 从连接池获取连接
        return "数据库连接对象"

# 测试
if __name__ == "__main__":
    # 无论调用多少次,都只会创建一个实例
    db1 = DatabaseManager("mysql://localhost:3306/test")
    db2 = DatabaseManager("mysql://localhost:3306/prod")
    
    print(db1 is db2)  # 输出: True
    print(db1.connection_string)  # 输出: mysql://localhost:3306/test
    print(db2.connection_string)  # 输出: mysql://localhost:3306/test

注意:饿汉式的__init__方法仍然会在每次实例化时被调用, 因此需要添加initialized标记来防止重复初始化。

5.2 基础懒汉式(Lazy Initialization)

延迟加载

核心特点:第一次调用获取实例的方法时才创建实例,实现了延迟加载。

优缺点:✅ 节省资源,只有在需要时才创建实例;❌ 非线程安全,多线程环境下可能会创建多个实例。

class Logger:
    _instance = None
    
    def __init__(self, log_file="app.log"):
        self.log_file = log_file
        print(f"日志记录器初始化完成,日志文件: {log_file}")
    
    @classmethod
    def get_instance(cls, log_file="app.log"):
        # 只有在第一次调用时才创建实例
        if not cls._instance:
            cls._instance = cls(log_file)
        return cls._instance
    
    def log(self, message):
        print(f"[{self.log_file}] {message}")

# 测试
if __name__ == "__main__":
    logger1 = Logger.get_instance("app.log")
    logger2 = Logger.get_instance("error.log")
    
    print(logger1 is logger2)  # 输出: True
    logger1.log("这是一条日志")  # 输出: [app.log] 这是一条日志
    logger2.log("这是另一条日志")  # 输出: [app.log] 这是另一条日志

警告:基础懒汉式在多线程环境下是不安全的。如果多个线程同时通过if not cls._instance判断, 就会创建多个实例,违反单例模式的初衷。

5.3 线程安全懒汉式(双重检查锁定)

⭐ 生产环境推荐

核心特点:在基础懒汉式的基础上增加了双重检查锁定(Double-Checked Locking)机制, 既保留了延迟加载的优点,又保证了线程安全。

优缺点:✅ 线程安全,延迟加载,性能好;❌ 实现相对复杂一点。

import threading

class ConfigManager:
    _instance = None
    _lock = threading.Lock()  # 线程锁
    
    def __init__(self):
        # 加载配置文件(只会执行一次)
        self.config = self._load_config()
        print("配置文件加载完成")
    
    def _load_config(self):
        # 实际的配置加载逻辑
        return {"debug": True, "port": 8000, "database": "mysql://localhost:3306/test"}
    
    @classmethod
    def get_instance(cls):
        # 第一次检查:如果实例已存在,直接返回,避免不必要的锁竞争
        if not cls._instance:
            # 获取锁,保证只有一个线程进入
            with cls._lock:
                # 第二次检查:防止多个线程同时通过第一次检查
                if not cls._instance:
                    cls._instance = cls()
        return cls._instance
    
    def get(self, key, default=None):
        return self.config.get(key, default)

# 多线程测试
def test_singleton():
    config = ConfigManager.get_instance()
    print(f"线程 {threading.get_ident()}: {id(config)}")

if __name__ == "__main__":
    # 创建10个线程同时获取实例
    threads = []
    for _ in range(10):
        t = threading.Thread(target=test_singleton)
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    # 所有线程都会得到同一个实例
    config1 = ConfigManager.get_instance()
    config2 = ConfigManager.get_instance()
    print(f"config1 is config2: {config1 is config2}")  # 输出: True

六、对比总结

实现方式 线程安全 延迟加载 代码复杂度 适用场景
饿汉式 ✅ 是 ❌ 否 实例一定会被使用、创建成本低
基础懒汉式 ❌ 否 ✅ 是 单线程环境、简单测试场景
线程安全懒汉式 ✅ 是 ✅ 是 生产环境、多线程应用

单例模式优点

  • 保证一个类只有一个实例,节省系统资源
  • 提供全局统一的访问点,方便管理
  • 可以精确控制实例的创建时机和生命周期
  • 避免了频繁创建和销毁对象的性能开销
  • 保证了全局状态的一致性

单例模式缺点

  • 违反单一职责原则,同时负责创建和业务逻辑
  • 引入全局状态,增加了代码的耦合度
  • 不利于单元测试,状态会在测试之间共享
  • 扩展困难,无法通过继承来扩展功能
  • 在多线程环境下需要额外处理线程安全

七、使用建议

  1. 生产环境优先使用线程安全懒汉式: 双重检查锁定的实现方式在性能、安全性和资源利用率之间取得了最好的平衡,是绝大多数生产环境下的首选。
  2. 简单场景可以使用饿汉式: 如果确定实例一定会被使用,且创建成本不高,饿汉式是最简单可靠的实现方式。
  3. 避免使用基础懒汉式: 基础懒汉式在多线程环境下存在严重的线程安全问题,除非是完全的单线程环境,否则不要使用。
  4. 避免滥用单例模式: 单例模式会引入全局状态,增加代码的耦合度和测试难度。只有在确实需要全局唯一实例时才使用,不要为了方便而滥用。
  5. 注意单例的生命周期: 单例实例一旦创建,会一直存在于内存中,直到应用程序退出。对于占用大量内存的单例,要注意及时释放不必要的资源。