单例模式解析

介绍

✅ 一、什么是单例模式?

一个类在整个程序运行过程中,只能被创建一次实例,且这个实例是全局共享的。

✅ 二、为什么要用单例模式?

适用于那些:

• 系统中只应该有一个实例存在的对象

• 该对象需要被多个地方共享使用(比如:配置类、数据库连接池、线程池、缓存、日志器

相关模式实现

⭐ 方式一:最经典的懒汉式(线程不安全)

只有在第一次调用时 才创建实例,之前不初始化

python 复制代码
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

🧠 所以它叫"懒汉":

• 懒:我不主动初始化

• 只有你来用我时,我才出手!

• 第一次调用时创建实例

• 之后返回同一个实例

问题:多线程下可能出现创建多个实例

🧠 表面看上去是"只初始化一次",但其实是 线程不安全的

在多线程并发环境下,有多个线程

几乎同时进入 new,几乎同时判断到 _instance is None,然后都去 new 一个对象

复制代码
•	线程A:判断 _instance is None ✅ → 正在执行 super().__new__(cls) 还没赋值;
•	同时线程B 也进来了,判断 _instance is None ✅ → 也执行 super().__new__(cls);
•	最终:两个对象都被创建,并都赋值给 _instance → 后来者覆盖前者;

线程A: new Singleton() -> instance_A

线程B: new Singleton() -> instance_B

最终 cls._instance = instance_B

虽然最后 cls._instance 只保留一个,但

已经有两个不同的对象被初始化过了!

与之相对的是饿汉模式:

程序一启动,就主动把实例创建好,无论你用不用,我都提前准备好了!

python 复制代码
class Singleton:
    ...

single_ton = Singleton()

优点:实现最简单,天然线程安全 ✅

缺点:不够懒惰,可能浪费资源 ❌(比如 RedisClient 在启动时并未使用)

模式 创建时机 优点 缺点
懒汉式 第一次使用时 节省资源 线程不安全(需加锁)
饿汉式 模块加载时就创建 实现简单、线程安全 无需立即用也会占资源

⭐ 方式二:线程安全(加锁)

python 复制代码
import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

双重检查锁(Double-Checked Locking)

保证线程安全 ✅

第一个 if 是快速通道:大多数情况下跳过锁,提高性能;

第二个 if 是防止两个线程都卡在锁前,进去后又创建两次;

with cls._lock 保证多线程下只创建一次实例!

⭐ 方式三:使用装饰器封装

python 复制代码
def singleton(cls):                  # 接收类作为参数
    instances = {}                   # 用于缓存创建过的实例

    def wrapper(*args, **kwargs):    # 实际替代原来的类构造器
        if cls not in instances:     # 如果没有创建过,就创建一次
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]        # 返回这个唯一实例

    return wrapper                   # 返回包装后的类构造函数

@singleton
class MyClass:
    pass
    
>>>>> 相当于:
MyClass = singleton(MyClass)
a = MyClass()
b = MyClass()
print(a is b)  # True ✅

这段代码的目的是让某个类 只能被实例化一次 ------ 后续所有实例化操作都会返回第一次创建的对象。

⚠️ 注意事项

  1. 无法重设参数:如果第一次传参错误了,后续也只能拿到那个错误参数的实例;

  2. 线程不安全:这个 instances 字典不是线程安全的,如果在高并发下要加锁(可以用 threading.Lock());

  3. 作用范围是进程级别:不是跨进程或分布式单例,如果你用 Gunicorn 等部署,每个 worker 都是独立的。

@singleton 装饰器是一种函数式实现单例模式,优雅、简单、可复用,适合绝大多数轻量场景。

⭐ 方式四:使用模块本身(Pythonic 最推荐)

python 复制代码
# config.py
value = {} # 定义一个全局变量 

# main.py
import config
config.value['a'] = 123

🌟 Python 的模块,其实就是天然的"单例"对象!

🧠 Python 模块加载机制:

  1. Python 会先去 sys.modules 查这个模块是否已经加载过。

  2. 如果没加载,就会执行一次 config.py,并把执行结果缓存下来;

  3. 如果以后其他地方再 import config,不会重新执行,而是直接使用缓存的模块对象

这就达到了"全局唯一、全局共享状态

说明
模块只初始化一次 Python 的 import 是惰性且缓存的
所有地方 import 的是同一个对象 模块对象在 sys.modules 中唯一
修改模块内部变量,所有引用方可见 所以可实现"全局共享状态"
非常适合做配置、状态管理、缓存等 比如 settings.py, global_store.py 等

✅ 示例升级:模拟配置中心

python 复制代码
# config.py
settings = {
    "env": "dev",
    "db": {}
}
python 复制代码
# service.py
import config

config.settings["db"]["host"] = "127.0.0.1"

# handler.py
import config
print(config.settings["db"]["host"])  # 127.0.0.1 ✅
实现方式 优势 典型用途
模块作为单例 简单优雅,天然支持 配置、全局变量、缓存对象等
类实现单例 更复杂,适合封装行为 RedisClient、Logger、连接池等

线程锁

线程锁是一种同步机制,用来确保多个线程访问共享资源时,不会发生冲突或竞争

📌 简单理解:

就像一个"🔒厕所门锁":

• 一次只能让一个人(线程)进去;

• 别人只能等着锁释放后才能进去;

• 避免两个线程同时进来把厕所搞炸了 🚽💥

在 Python 多线程中,多个线程是同时执行的(虽然有 GIL,但 I/O 场景或内部切换是并发的)。

python 复制代码
import threading

lock = threading.Lock()

def safe_task():
    with lock:
        # 这里是线程安全的代码块
        # 同一时刻只有一个线程能执行
        global counter
        counter += 1
        
>>>>>>>>>
lock.acquire()
try:
    # 临界区
    counter += 1
finally:
    lock.release()

2️⃣ 控制共享资源访问(如:写文件、更新数据库、内存计数器)

方法 含义
lock.acquire() 请求锁,如果被其他线程持有,则阻塞直到获得
lock.release() 释放锁,让其他线程可以进入
with lock: 上下文写法,自动 acquire 和 release

注意:

说明
死锁 如果获取了锁却忘记释放,就会造成别的线程永远卡住
加锁粒度 不要加得太宽,否则会导致程序性能下降(串行化)
多线程操作对象 加锁保护的对象必须是共享变量,避免没必要的加锁

threading.Lock() 是 Python 中用来控制多线程"访问共享资源"的原始同步工具,相当于设置一个"互斥区",同一时间只允许一个线程进去执行关键逻辑。

场景

场景 描述
日志系统 所有模块写日志用同一个 Logger
数据库连接池 只初始化一次,避免频繁连接
配置管理类 读取一次配置,全局共享
缓存客户端(如 RedisClient) 保持单个连接池对象

• 在多线程/多进程下使用,必须注意线程安全;

• 单例生命周期长,避免持有过多状态(会引起"脏数据");

• 使用不当可能导致代码耦合度高,不利于测试(尤其是自动化测试中会污染状态)。

相关推荐
找了一圈尾巴5 小时前
设计模式(结构性)-代理模式
设计模式·代理模式
Debug 熊猫5 小时前
【Java基础】10章、单例模式、final关键字的使用技巧和使用细节、单例模式-懒汉式、单例模式-饿汉式【3】
java·javascript·后端·单例模式
渊渟岳6 小时前
掌握设计模式--模板方法模式
设计模式
程序员JerrySUN21 小时前
设计模式 Day 2:工厂方法模式(Factory Method Pattern)详解
设计模式·工厂方法模式
每次的天空1 天前
Android 单例模式全解析:从基础实现到最佳实践
android·单例模式
牵牛老人1 天前
C++设计模式-迭代器模式:从基本介绍,内部原理、应用场景、使用方法,常见问题和解决方案进行深度解析
c++·设计模式·迭代器模式
诺亚凹凸曼1 天前
23种设计模式-结构型模式-组合
设计模式
诺亚凹凸曼1 天前
23种设计模式-结构型模式-桥接器
android·java·设计模式
xyliiiiiL1 天前
单例模式详解
java·开发语言·单例模式
却尘1 天前
跨域资源共享(CORS)完全指南:从同源策略到解决方案 (1)
前端·设计模式