当前位置: 首页 > article >正文

懒汉式单例模式

懒汉式单例是一种在需要时才会初始化实例的单例模式实现方式,适用于需要延迟加载的场景。以下是一个实际使用懒汉式单例的例子,并结合适用场景进行解析。


示例场景:日志管理器

在开发过程中,日志记录是一个常见需求,通常日志记录器在整个应用中只需要一个实例。使用懒汉式单例可以确保日志管理器只在第一次需要时进行初始化,从而节省系统资源。

懒汉式单例完整代码
public class LogManager {
    // 1. 静态变量,保存唯一实例,但不立即初始化
    private static LogManager instance = null;

    // 2. 私有构造方法,防止外部实例化
    private LogManager() {
        System.out.println("LogManager initialized!");
    }

    // 3. 提供一个静态方法访问唯一实例
    public static synchronized LogManager getInstance() {
        if (instance == null) {
            instance = new LogManager();  // 延迟实例化
        }
        return instance;
    }

    // 4. 示例方法,用于记录日志
    public void log(String message) {
        System.out.println("Log: " + message);
    }
}

代码解析

  1. 静态变量 instance

    • 静态变量 instance 用于保存 LogManager 类的唯一实例。
    • 初始值为 null,实例化操作延后到第一次调用 getInstance() 时才进行。
  2. 私有构造方法:

    • 构造方法被声明为 private,防止外部通过 new LogManager() 创建实例。
    • 在构造方法中可以放置初始化逻辑,例如配置日志文件路径等。
  3. 静态方法 getInstance()

    • 是懒汉式单例的核心,通过 synchronized 关键字保证线程安全。
    • 第一次调用时,instancenull,会创建一个新的实例;
      后续调用时,直接返回已经创建的实例。
  4. 功能性方法 log()

    • 提供具体的业务功能,例如记录日志。

使用示例

假设我们需要记录一些重要的操作日志,可以通过以下代码来使用 LogManager

public class Main {
    public static void main(String[] args) {
        // 第一次调用时实例化 LogManager
        LogManager logger1 = LogManager.getInstance();
        logger1.log("This is the first log message.");

        // 第二次调用时直接返回已有实例
        LogManager logger2 = LogManager.getInstance();
        logger2.log("This is the second log message.");

        // 比较两个实例
        System.out.println("Are logger1 and logger2 the same instance? " + (logger1 == logger2));
    }
}

输出结果

LogManager initialized!
Log: This is the first log message.
Log: This is the second log message.
Are logger1 and logger2 the same instance? true
说明:
  • 第一次调用 LogManager.getInstance() 时,LogManager 被初始化(输出 "LogManager initialized!")。
  • 第二次调用 getInstance() 时,只是返回已有实例,没有再次创建新实例。
  • 比较两个实例,结果为 true,表明它们是同一个对象。

懒汉式单例的优缺点

优点
  1. 延迟加载
    • 实例在第一次使用时才创建,节省内存和系统资源。
  2. 线程安全性
    • 使用 synchronized 保证线程安全。
缺点
  1. 性能问题
    • 每次调用 getInstance() 都需要进入同步块,会带来一定的性能开销。在性能敏感的场景下可能不够高效。

改进方案:双重检查锁定(Double-Checked Locking)

为了解决同步带来的性能问题,可以使用双重检查锁定优化懒汉式单例:

public class LogManager {
    // 1. 静态变量,使用 volatile 修饰以保证可见性
    private static volatile LogManager instance = null;

    // 2. 私有构造方法
    private LogManager() {
        System.out.println("LogManager initialized!");
    }

    // 3. 提供静态方法,使用双重检查锁定
    public static LogManager getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (LogManager.class) {
                if (instance == null) { // 第二次检查
                    instance = new LogManager();
                }
            }
        }
        return instance;
    }

    // 示例功能
    public void log(String message) {
        System.out.println("Log: " + message);
    }
}
优势
  • 第一次检查和第二次检查减少了不必要的同步,提高了性能。
  • 使用 volatile 关键字保证多线程环境下的可见性,防止指令重排序导致的错误。
扩展1 — 双重检查锁定

这段代码是一个双重检查锁定(Double-Checked Locking)实现的懒汉式单例模式的核心部分,它旨在解决多线程环境下单例实例创建的线程安全问题,同时优化性能。下面是对这段代码的详细解析:

  1. 第一次检查 (if (instance == null)):

    • 目的:快速判断实例是否已经创建。
    • 优点:如果实例已经存在,则可以直接返回实例,避免进入同步块,提高了性能。
  2. 同步块 (synchronized (LogManager.class)):

    • 目的:保证在多线程环境下只有一个线程能够进入块内创建实例,确保线程安全。
    • synchronized用在类对象上,确保同一时刻只有一个线程可以初始化实例。
  3. 第二次检查 (if (instance == null)):

    • 目的:在同步块内再次检查实例是否为 null
    • 原因:在第一次检查之后进入同步块之前,可能有其他线程已经创建了实例,因此需要再次检查以防止重复创建。
  4. 实例化 (instance = new LogManager()):

    • 当且仅当 instance 确实为 null 且当前线程获得了同步锁时,才创建实例。
    • 确保 LogManager 的实例只被创建一次。
  5. 返回实例 (return instance):

    • 无论是通过快速路径(无锁)还是同步路径(加锁),最终都会返回唯一的实例。

Why Double-Checked Locking?

  • 性能优化

    • 通过双重检查,减少了进入同步锁的次数。只有在 instancenull 时,才会进入同步块。一般情况下(即实例已经创建后),只需要执行第一次检查即可返回实例,无需同步。
  • 线程安全

    • 同步块保证了只有一个线程可以执行实例初始化。即使多个线程同时发现 instancenull,由于同步的存在,最终只有一个线程能够创建实例。
扩展2 — 使用 volatile

为了完全保证双重检查锁定的正确性,instance 应该使用 volatile 关键字声明:

private static volatile LogManager instance = null;
  • 作用
    • 防止指令重排序1:确保 new LogManager() 操作的顺序正确,即先分配内存,再执行构造函数,最后将内存地址赋值给 instance
    • 变量在多个线程之间的可见性:一旦一个线程修改了 instance,其他线程能够立即看到变化。

双重检查锁定模式是实现懒汉式单例的一种高效方式,适用于性能要求较高的多线程环境。但是,它的正确实现依赖于 volatile 关键字来防止重排序问题。通过这种方式,我们可以在保证线程安全的同时,尽量减少同步带来的性能损耗。


总结

懒汉式单例适合于需要延迟加载且实例化成本较高的场景(如日志管理器、配置加载器等)。在并发场景下,最好使用线程安全的实现,例如同步方法版或双重检查锁定版,以确保唯一实例的正确性和性能的平衡。


  1. 指令重排序:编译器和处理器在执行程序时可能会对指令进行重排序,以优化性能。volatile 关键字会禁止这种重排序,确保变量的初始化和其他操作的执行顺序符合程序的预期。
    这对于实现线程安全的懒汉式单例模式非常重要,因为它保证了对象在初始化完成后才会被其他线程看到。 ↩︎


http://www.kler.cn/a/504000.html

相关文章:

  • C#使用OpenTK绘制3D可拖动旋转图形三棱锥
  • 东芝3525AC彩色复印机复印默认成黑白模式方法
  • 【Rust】控制流
  • 图解Git——分支的新建与合并《Pro Git》
  • Web前端界面开发
  • QT Quick QML 实例之椭圆投影,旋转
  • 当当网热销书籍数据采集与可视化分析
  • 松散比较(PHP)(小迪网络安全笔记~
  • MySQL数据库(SQL分类)
  • WSL报错libcudnn_cnn_infer.so.8
  • 基于Hiperwalk的量子行走Python编程
  • 基于celery的任务管理,本文主要是处理自己的算法,暴露API,管理任务并发,多线程
  • LeetCode 2657. Find the Prefix Common Array of Two Arrays
  • SCDN跟高防IP相比哪个更好
  • 计算机视觉算法实战——实时车辆检测和分类(主页有相关源码)
  • 大语言模型训练的基本步骤解析
  • llama.cpp 模型可视化工具 GGUF Visualizer
  • 提高互联网Web安全性:避免越权漏洞的技术方案
  • 在Visual Studio中编译.c文件和.cpp文件主要有哪些不同
  • 第三篇 Avaya IP Office的架构及其服务组成
  • Mysql--运维篇--安全性(数据库访问控制,最小权限原则,表空间加密,TLS加密,证书签发,SQL注入及防范等)
  • centos 8 中安装Docker
  • [读书日志]8051软核处理器设计实战(基于FPGA)第七篇:8051软核处理器的测试(verilog+C)
  • 多商家入驻商城系统架构与功能分析
  • 《鸿蒙Next旅游应用:人工智能赋能个性化与智能导览新体验》
  • workloadSelector 是一种在服务网格(如Istio)中用于选择特定工作负载实例的机制。