4、单例模式实现(二):懒汉式单例的Python实现、双重检查锁机制

上一节我们聊了饿汉式单例。说白了就是程序一启动,实例就乖乖等着了。但现实开发中,很多时候我们并不希望它这么积极——万一这个对象初始化特别重,而用户可能根本用不上呢?

这时候,懒汉式就派上用场了。它的核心思想就一句话:等到真正需要的时候,再创建实例

懒汉式的基本实现

先看一个最朴素的版本。我刚开始学设计模式时,第一个写的懒汉式大概长这样:

class SingletonLazy:
    _instance = None

    def __init__(self):
        if SingletonLazy._instance is not None:
            raise Exception("哥们,别重复创建了,用 get_instance() 吧")
        # 这里放真正的初始化逻辑
        self.data = "我是懒汉式单例"

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

嗯,逻辑上没问题。第一次调用 get_instance() 时,检查 _instance 是不是 None,是就创建,不是就直接返回。

但这里有个坑。我在一个高并发项目里用过这个写法,结果线上出了诡异的问题——两个线程同时调 get_instance(),都发现 _instance is None,然后各自创建了一个实例。单例不单了,你说尴尬不尴尬?

⚠️ 线程安全问题:上述代码在多线程环境下不是线程安全的。两个线程可能同时进入 if 判断,导致创建多个实例。

加锁解决线程安全

最简单的解决办法,就是给方法加一把锁。Python 里用 threading.Lock 就行:

import threading

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

    def __init__(self):
        if SingletonThreadSafe._instance is not None:
            raise Exception("别重复创建了")
        self.data = "线程安全的懒汉式"

    @classmethod
    def get_instance(cls):
        with cls._lock:
            if cls._instance is None:
                cls._instance = cls()
        return cls._instance

这样确实安全了。但代价是什么?每次调用 get_instance() 都要拿锁。你想想看,在高并发场景下,成千上万个线程抢同一把锁,性能损耗可不小。

我记得有一次压测,这个加锁版本比不加锁的慢了将近 10 倍。虽然单例创建只有一次,但读操作每次都要等锁释放,太冤了。

双重检查锁机制(Double-Checked Locking)

那有没有办法既保证线程安全,又避免每次调用都加锁?有,就是双重检查锁。

思路很简单:先不加锁检查一次,如果实例已经存在,直接返回。只有发现实例为 None 时,才去拿锁。拿到锁之后,再检查一次——这就是「双重检查」的由来。

import threading

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

    def __init__(self):
        if SingletonDCL._instance is not None:
            raise Exception("别重复创建了")
        self.data = "双重检查锁单例"

    @classmethod
    def get_instance(cls):
        # 第一次检查:不加锁
        if cls._instance is None:
            with cls._lock:
                # 第二次检查:加锁后
                if cls._instance is None:
                    cls._instance = cls()
        return cls._instance

这个模式在 Java 里非常流行。Python 里用起来也顺手。为什么它能提升性能?因为大部分情况下,实例已经创建好了,第一次检查就直接返回了,根本不需要碰锁。

💡 核心要点:双重检查锁的关键在于「先判断,后加锁,再判断」。第一次判断避免不必要的加锁,第二次判断保证线程安全。

Python 中的特殊注意事项

这里我要多说一句。Python 的 threading.Lock 本身是可靠的,但双重检查锁在 Python 中有一个微妙的问题——指令重排

在 C++ 或 Java 里,编译器或 CPU 可能会对指令进行重排序,导致一个线程看到 _instance 不为 None,但对象还没完全初始化。Python 的 GIL(全局解释器锁)在一定程度上缓解了这个问题,但并不是完全免疫。

我个人习惯的做法是:如果对性能要求极高,而且确定是 Python 3.2+,可以用 threading.local 或者干脆用模块级别的单例。但如果你就是想用双重检查锁,也没问题——绝大多数场景下,Python 的 GIL 会帮你兜底。

📌 小技巧:在 Python 中,还有一种更优雅的懒汉式实现——使用 __new__ 方法。但双重检查锁的写法更通用,跨语言迁移时更容易理解。

懒汉式 vs 饿汉式:怎么选?

我遇到过不少团队在选型时纠结这个问题。其实没那么复杂,我列个表你就明白了:

对比维度 饿汉式 懒汉式(双重检查锁)
实例创建时机 类加载时 首次调用时
线程安全 天然安全 需要额外处理
启动速度 较慢(要初始化) 较快(延迟加载)
运行时性能 无锁,极快 首次加锁,后续无锁
适用场景 实例轻量、一定会用 实例重量、可能不用

说白了,如果你的单例对象初始化很轻,而且程序启动后几乎一定会用到它,那就用饿汉式,省心。如果对象初始化很重(比如加载配置文件、建立数据库连接池),或者不确定用户会不会用,那就用懒汉式加双重检查锁。

懒汉式单例的核心逻辑图

下面这张图,把整个流程串了一遍。我建议你仔细看看,尤其是「第一次检查」和「第二次检查」的分支逻辑:

懒汉式单例 + 双重检查锁 流程图 调用 get_instance() 第一次检查 _instance is None? 直接返回实例 获取锁 (with _lock) 第二次检查 _instance is None? 返回已有实例 创建实例并返回 判断节点 操作节点 加锁操作 返回操作

从图上你能看到,整个流程有两个关键判断点。第一次判断是为了「快速失败」——实例已经有了就直接走人。第二次判断是为了「防止重复创建」——两个线程都通过了第一次检查,但只有拿到锁的那个才能创建实例。

避坑指南

最后,分享几个我踩过的坑:

  • 别在 __init__ 里做太多事:我曾经把一个数据库连接池的初始化放在 __init__ 里,结果第一次调用时卡了好几秒。建议把重量级初始化放到一个单独的方法里,按需调用。
  • 小心序列化破坏单例:如果你的单例需要序列化,记得实现 __reduce__ 或使用 __new__ 控制。否则反序列化时会创建新实例。
  • 不要滥用单例:我见过一个项目,几乎每个类都是单例。最后代码耦合得一塌糊涂,改一个地方牵一发动全身。单例虽好,可别贪杯。

嗯,懒汉式单例和双重检查锁就聊到这儿。这个模式在实际项目中非常实用,尤其是那些「可能用也可能不用」的重型资源。记住核心思路:延迟加载 + 双重检查,既省资源又保安全。


公众号:蓝海资料掘金营,微信deep3321