脚本如何保证多线程单例安全

wen 实用脚本 28

本文目录导读:

脚本如何保证多线程单例安全

  1. 推荐:静态内部类(Initialization-on-demand Holder)
  2. 最安全:使用枚举(Effective Java 作者推荐)
  3. 经典方式:双重检查锁定(DCL)
  4. 简单但较耗性能:饿汉式
  5. 不推荐:同步方法
  6. 总结与选择建议

保证多线程环境下单例模式的安全,核心思路是确保实例化过程是原子性的、可见性的,并且禁止指令重排序

以下是几种常见的实现方式,按推荐程度从高到低排序:

推荐:静态内部类(Initialization-on-demand Holder)

这是最优雅、最高效的方式,它利用了 Java 类加载机制的延迟加载线程安全特性。

原理:

  • 外部类被加载时,内部类 SingletonHolder 不会被加载。
  • 只有调用 getInstance() 时,内部类才会被加载,JVM 会保证 INSTANCE 的初始化和赋值是线程安全的。
  • 类加载机制天然避免了同步开销。
public class Singleton {
    // 私有构造函数,防止外部实例化
    private Singleton() {}
    // 静态内部类
    private static class SingletonHolder {
        // final 确保内存可见性
        private static final Singleton INSTANCE = new Singleton();
    }
    // 公共的获取实例方法
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

优点: 高性能、线程安全、延迟加载、代码简洁。 缺点: 无法在构造函数中传递参数(如果有这个需求,需要考虑其他方式)。


最安全:使用枚举(Effective Java 作者推荐)

Java 枚举本身就是天然的单例,JVM 保证了其线程安全和防反射攻击。

原理:

  • 枚举的构造方法默认是私有的。
  • JVM 在加载枚举类时,会以线程安全的方式创建定义的枚举常量。
  • 枚举序列化时,反序列化不会创建新对象(普通单例类需要额外实现 readResolve 方法)。
public enum Singleton {
    INSTANCE;
    // 可以添加其他方法
    public void doSomething() {
        System.out.println("Doing something safely in multi-thread!");
    }
}
// 使用方式:
Singleton.INSTANCE.doSomething();

优点: 极其简单、绝对线程安全、防反射、防序列化破坏。 缺点: 不是“懒加载”(但如果枚举很小,通常可以忽略);不够“面向对象”(有时不太习惯)。


经典方式:双重检查锁定(DCL)

适用于需要延迟加载且无法使用静态内部类或枚举的场景。必须搭配 volatile 关键字

原理:

  1. 第一次检查: 如果实例已存在,直接返回,避免同步开销。
  2. 同步块: 只有第一次创建时,多个线程可能进入此块。
  3. 第二次检查: 防止在等待锁的过程中,另一个线程已经创建了实例。
  4. volatile 禁止指令重排序。new Singleton() 操作不是原子的(分配内存 -> 初始化对象 -> 指向引用),如果不加 volatile,另一个线程可能看到一个未完全初始化的对象。
public class Singleton {
    // 关键点:volatile 禁止指令重排序
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        // 第一次检查
        if (instance == null) {
            // 类锁
            synchronized (Singleton.class) {
                // 第二次检查
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

优点: 延迟加载、性能在实例创建后接近无锁。 缺点: 代码稍复杂,volatile 会略微降低性能(相对于静态内部类),且容易因为忘记加 volatile 而出 bug。


简单但较耗性能:饿汉式

在类加载时就创建实例,JVM 保证了类加载过程的线程安全。

原理: 利用 JVM 类加载机制,无论是否使用,都会创建实例。

public class Singleton {
    // 类加载时就创建
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优点: 实现简单,无需处理复杂同步。 缺点: 不是懒加载,如果创建开销很大或可能不被用到,会浪费资源。


不推荐:同步方法

给整个 getInstance 方法加 synchronized

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    // 方法级同步,性能极差
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

原因: 每次调用 getInstance() 都会获得锁,即使实例已经存在,在高并发场景下,性能会严重下降。


总结与选择建议

实现方式 线程安全 懒加载 性能 推荐指数 适用场景
静态内部类 ✅ 完美 ✅ 是 ⭐⭐⭐ 最高 首选 大部分通用场景
枚举 ✅ 完美 ❌ 否(预加载) ⭐⭐⭐ 最高 首选 绝对安全和简单的场景
双重检查锁定 (DCL) ✅ 需配合volatile ✅ 是 ⭐⭐ 高 次选 需要延迟加载且无法用内部类的场景
饿汉式 ✅ 天然 ❌ 否 ⭐⭐⭐ 最高 接受 实例轻量、肯定会用到的场景
同步方法 ✅ 是 ❌ 极差 不推荐 仅学习用途

如果你的脚本是线程安全的,通常意味着需要显式处理同步,但很多“脚本”语言(如 Python、JavaScript)的单例实现方式与 Java 不同。

Python 示例(线程安全):

import threading
class Singleton:
    _instance = None
    _lock = threading.Lock()
    def __new__(cls, *args, **kwargs):
        # 双重检查锁定(Python中也可以)
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

JavaScript(Node.js)注意: JS 是单线程事件循环,通常不需要担心多线程并发问题(除非用了 Worker Threads),标准模块导入(import / require)本身就是单例且安全的。

抱歉,评论功能暂时关闭!