懒汉式单例模式

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


示例场景:日志管理器

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

懒汉式单例完整代码
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 关键字保证线程安全。
    • 第一次调用时,instance 为 null,会创建一个新的实例;
      后续调用时,直接返回已经创建的实例。
  4. 功能性方法 log():

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

使用示例

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

java 复制代码
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));
    }
}

输出结果

plaintext 复制代码
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?

  • 性能优化:

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

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

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

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

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


总结

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


  1. 指令重排序:编译器和处理器在执行程序时可能会对指令进行重排序,以优化性能。volatile 关键字会禁止这种重排序,确保变量的初始化和其他操作的执行顺序符合程序的预期。

    这对于实现线程安全的懒汉式单例模式非常重要,因为它保证了对象在初始化完成后才会被其他线程看到。 ↩︎

相关推荐
kimnoic1 小时前
Python数据库学习心得:SQLite、MySQL、PostgreSQL优缺点
开发语言·python
别动我齐刘海2 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
必须得开心呀2 小时前
python编写带界面的软件,需要安装哪些插件
开发语言·python
code2cat3 小时前
Java进阶篇之AtomicLongFieldUpdater:把累计额度留在对象字段中
java·cas·并发编程
WeiXin_DZbishe3 小时前
基于springboot城市公交实时运营与调度系统-计算机毕设 附源码【课程设计】66173
java·vue.js·spring boot·后端·python·php·课程设计
泡茶喝茶写代码3 小时前
A股量化数据工程:从 REST 接口到策略信号(第 18 篇):放量缩量异动扫描
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
wewowilling3 小时前
认识微服务
java·运维·微服务·springcloud
代码什么用4 小时前
Java Web 7-10
java·servlet
沫璃染墨4 小时前
《Qt从零入门系列(十三):QSS样式表详解——从选择器与层叠规则到控件美化实战》
开发语言·css·c++·qt·ui·qt6.3·qss