懒汉式单例模式

devtools/2025/1/15 20:53:58/

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


示例场景:日志管理器

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

懒汉式单例完整代码
java">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

java">public class Main {public static void main(String[] args) {// 第一次调用时实例化 LogManagerLogManager 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)

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

java">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 关键字声明:

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

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


总结

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


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


http://www.ppmy.cn/devtools/150758.html

相关文章

fastgpt 调用api 调试 写 localhost, 127.0.0.1不行,要 ipconfig 找到本机ip

fastgpt 调用api 调试 写 localhost, 127.0.0.1不行,要 ipconfig 找到本机ip IPv4 地址 . . . . . . . . . . . . : 192.168.1.2

在vscode中使用R-1

参考我的上一篇博客: https://blog.csdn.net/weixin_62528784/article/details/145092632?spm1001.2014.3001.5501 这篇内容实际上就是上一篇博客的后续承接,既然都在vscode的jupyter中使用R了,实际上其实也能够直接在vscode中原生使用R的编…

NHANES数据挖掘|特征变量对死亡率预测的研究设计与分析

书接上回,应各位临床或在科室的小伙伴们需求,除了多组学和算法开发外,插播关于临床护理方向的数据挖掘,今天分享两篇NHANES的分析文献。 1、时依中介分析 DOI: 10.1186/s12933-024-02191-5 整体思路 基于 NHANES 数据…

网络层协议-----IP协议

目录 1.认识IP地址 2.IP地址的分类 3.子网划分 4.公网IP和私网IP 5.IP协议 6.如何解决IP地址不够用 1.认识IP地址 IP 地址(Internet Protocol Address)是指互联网协议地址。 它是分配给连接到互联网的设备(如计算机、服务器、智能手机…

机器学习 - 常用的损失函数(0-1、平方)

损失函数是一个非负实数函数,用来量化模型预测和真实标签之间的差异. 下面介绍几种常用的损失函数. 一、0-1损失函数 在机器学习和统计学中,0-1损失函数是一种简单而直观的损失函数,用于衡量预测值是否与实际值一致。其公式定义为&#xf…

mysql概述

sql的定义: sql(Structured Query Language):结构化查询语言 sql的分类: DDL(Data Definition Language): 数据定义语言——定义对数据库对象(库,表&…

微软组建新内部 AI 研发组织:开启智能创新新篇章

在科技飞速发展的当下,人工智能已成为全球各大科技巨头竞相角逐的核心赛道。近日,微软的一项重大举措引发了行业内的广泛关注 —— 成立了一个全新的专注于开发的内部人工智能组织。这一战略布局不仅彰显了微软在 AI 领域持续深耕的决心,更预…

Electron 图标修改

1. 窗口图标修改 在 Electron 的主进程代码中,通常是main.js文件,在创建BrowserWindow实例时,使用icon选项来设置窗口图标。 const { BrowserWindow } require("electron");const path require("path");function cre…