懒汉式单例模式

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


示例场景:日志管理器

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

懒汉式单例完整代码
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) {
        // 第一次调用时实例化 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?

  • 性能优化

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

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

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

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

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


总结

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


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

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

相关推荐
S-X-S1 分钟前
自定义异常模块
java·开发语言·spring boot
重生之Java开发工程师2 分钟前
Java中Map常用遍历方式以及性能对比
java·开发语言
Xiezequan3 分钟前
c++ 手写queue循环队列
开发语言·c++
成为编程高手O_o4 分钟前
Redis 实战篇 ——《黑马点评》(中)
java·数据库·redis·学习·缓存
SomeB1oody8 分钟前
【Rust自学】12.6. 使用TDD(测试驱动开发)开发库功能
开发语言·后端·重构·rust
一二小选手38 分钟前
【SpringSecurity】SpringSecurity安全框架授权
java·springsecurity·安全框架
上海拔俗网络1 小时前
“AI 自动化效能评估系统:开启企业高效发展新征程
java·团队开发
比特在路上1 小时前
初阶数据结构【队列及其接口的实现】
c语言·开发语言·数据结构
多多*1 小时前
JUC Java并发编程 高级 学习大纲 动员
java·开发语言·学习·面试·架构·bash·intellij-idea
盖世英雄酱581361 小时前
同事的问题代码(第四期)
java·后端