Java线程安全的单例模式如何实现 双重检查锁定原理

单例模式实现线程安全,双重检查锁定(Double-Checked Locking)它是一种常见而有效的写作方法。它不仅保证了性能,而且保证了多线程环境中唯一的例子。

单例模式的基本要求

单例模式的核心是:一个类别只允许创建一个例子,并提供一个全球访问点。在多线程场景中,必须防止多线程同时创建多个例子。

常见的实现方式包括:

  • 饿汉风格:类加载初始化,线程安全,但可能浪费资源
  • 懒汉风格:只有在第一次使用时才能创建,线程安全问题需要处理
  • 双重检查锁:延迟加载 + 高效的线程安全

实现代码的双重检查锁定

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; } }

双检锁定原理分析

这种写作方法之所以有效,是因为它降低了同步成本。只有在实例没有创建时才会锁定。一旦创建完成,后续调用将直接返回引用,无需同步。

  • 第一次检查 instance == null:如果存在实例,直接返回,不进入同步块,提高性能
  • synchronized 确保只有一个线程能同时进入临界区
  • 第二次检查 instance == null:防止多个线程在第一次检查后进入同步块,导致重复创建
  • volatile 防止对象创建过程中的指令重新排序

new Singleton() 不是原子操作,大致分为三个步骤:

  1. 内存空间的分配
  2. 调用构造函数的初始对象
  3. 将 instance 指向内存地址

由于 JVM 对于处理器的优化,可能会对指令进行重新排序,如执行第一步和第三步,然后执行第二步。此时,如果此时正好读取另一个线程 instance,会发现它非 null,但对象还没有完全初始化,导致错误。

volatile 这种重排序可以被禁止,以确保对象在结构完成前不会被其他线程看到。

为什么不加 volatile 会有问题?

在没有 volatile 在修改过程中,线程A在创建对象时进行了重新排序,线程B可能会被“半初始化”对象引用,并且在调用其方法时会出现不可预测的行为。此外 volatile 后,JVM 将内存屏障插入其他线程,以确保正确的结构顺序 always 是完整构造后的例子。