Java 单例模式

在 Java 中,单例模式(Singleton)用于保证某个类在整个应用中只有一个实例,并提供全局访问点

根据线程安全和实例化时机,可以分为懒汉式、饿汉式以及更高级的实现方式

1. 懒汉式单例(Lazy Initialization)

1.1 懒汉式,线程不安全

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 特点:延迟初始化,只在第一次调用 getInstance() 时创建实例

  • 缺点:多线程环境下可能产生多个实例,需要加锁

1.2 懒汉式,线程安全(同步方法)

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 特点:synchronized 保证同一时间只有一个线程执行 getInstance()

  • 缺点:同步整个方法效率低,99%情况下不需要同步

1.3 仅锁定实例化部分

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

问题:两个线程同时进入 if (instance == null),第一个线程创建实例后,第二个线程没有再次判断就可能重复实例化

1.4 双重检查锁定(Double-Check Locking)

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次检查
            synchronized (Singleton.class) {    // 同步块
                if (instance == null) {         // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

问题分析

  1. 可见性问题
    • Java 内存模型(JMM)中,每个线程有自己的工作内存,修改共享变量可能不会立即被其他线程看到
    • synchronized 保证:
      1. 进入同步块时,会从主内存刷新最新值到工作内存
      2. 退出同步块时,会将修改写回主内存
    • 因此,线程二在进入同步块时可以看到线程一创建的 Singleton 实例,可见性问题得以解决
    • 这里的“看到最新的值”指的是线程二在同步块内读取 instance 时,能够获取线程一已经创建好的对象,而不是旧的 null 或半初始化状态(假设使用 volatile 或 JDK 1.5+)
  2. 无序性问题(JVM 内存模型导致)
    • new Singleton() 并非原子操作,可分为:
      1. 分配内存
      2. 初始化对象
      3. 赋值给变量
    • JVM 1.4 及以前,为了性能优化,步骤可能被重排为:
      1. 分配内存
      2. instance 指向内存
      3. 初始化对象
    • 风险
      • 线程 T1 执行到步骤 2(instance 指向内存)
      • 线程 T2 进入第一次检查 if (instance == null),发现 instance != null
      • T2 直接使用 instance,此时对象尚未初始化 → 程序可能崩溃或行为异常

总结:双重检查锁定失败的根本原因是 JVM 内存模型允许无序写入,而不是锁本身或线程切换问题

解决方案

  1. 使用 volatile(JDK 1.5+)

    • 保证写入有序:先初始化对象,再赋值给变量

      private volatile static Singleton instance = null;
  2. 使用静态内部类 (IODH)

  • JVM 保证类加载初始化时线程安全
  1. 使用枚举
  • JVM 保证枚举实例线程安全且唯一

1.5 使用 volatile 避免无序性

public class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
  • JDK 1.5+:volatile 可以保证 new 操作的有序性,避免双重检查锁定的问题

  • JDK 1.4 及以前:volatile 只保证可见性,但无法保证有序性

1.6 双重检查锁定适用场景

  • 对基本类型(long, double)适用
  • 对引用类型,必须使用 volatile 或其他同步手段
private int count;
public int getCount() {
    if (count == 0) {
        synchronized(this) {
            if (count == 0) {
                count = computeCount(); // 耗时计算
            }
        }
    }
    return count;
}

2. 饿汉式单例(Eager Initialization)

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
  • 特点:在类加载时即创建实例,不需要同步

  • 优点:简单、线程安全

  • 缺点:提前占用资源,延迟加载不可用

2.1 Initialization on Demand Holder (IODH)

public class Singleton {
    private static class SingletonHolder {
        static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
  • 原理:内部静态类只有在被引用时才加载
  • 优点:延迟加载 + 天然线程安全

2.2 枚举方式(Enum Singleton)

private enum EnumSingleton {
    INSTANCE;

    private Singleton singleton;

    private EnumSingleton() {
        singleton = new Singleton();
    }

    public Singleton getInstance() {
        return singleton;
    }
}
  • 优点:JVM 保证枚举实例绝对唯一且线程安全,避免反射破坏单例

3. JVM 内存模型改进

  • JSR-133:Java 内存模型改进后,volatile 可以保证双重检查锁定正常工作
private volatile Resource resource;

public Resource getResource() {
    if (resource == null) {
        synchronized(this) {
            if (resource == null) {
                resource = new Resource();
            }
        }
    }
    return resource;
}
  • 总结:双重检查锁定失败的原因在于 JVM 内存模型允许无序写入,而非 JVM Bug

4. 总结与建议

  1. 懒汉式适用于单线程或延迟加载场景,但多线程下实现复杂
  2. 饿汉式最简单安全,适合 JVM 特性
  3. IODH 和枚举是现代 Java 推荐方式,兼顾延迟加载和线程安全
  4. 选择单例实现方式时:应根据应用场景、资源占用和线程要求进行选择,而非盲目追求懒加载或花哨的同步方式

JSR-133

1. JSR-133 背景

  • JVM 1.4 及以前的 Java 内存模型(JMM)允许指令重排无序写入,即便使用 synchronized,也无法保证双重检查锁定中的 new 操作有序
  • 这导致了 DCL 失败问题:一个线程可能看到 instance 已经非 null,但对象尚未初始化

双重检查锁定失败的核心是“写操作无序”和“共享变量的可见性”问题

2. JSR-133 的改进

JSR-133 是 Java 内存模型的一个重要修改,于 JDK 1.5 引入,主要改进包括:

2.1 引入 happens-before 语义

  • 定义了线程之间操作的顺序规则
  • 核心原则:
    1. 解锁先行发生于后续的锁定:同步块释放锁的操作先行发生于其他线程获取相同锁
    2. 写操作先行发生于随后的 volatile 读:对 volatile 的写入操作,先行发生于后续读取同一个 volatile 变量的操作

通过这些规则,JMM 明确了同步块和 volatile 变量的可见性和有序性保证

2.2 volatile 的增强

在 JDK 1.5 之前,volatile 只能保证可见性不保证写入顺序 JSR-133 改进后:

  • 保证有序性
    • volatile 写操作不会与前面的普通写操作重排
    • volatile 读操作不会与后面的普通读操作重排
  • 这样,new Singleton() 中:
    • 对象初始化完成后,才将引用写入 volatile instance
    • 避免线程看到“半初始化对象”

2.3 synchronized 的保证

  • synchronized 的锁机制仍然存在,增加了happens-before 保证
    • 锁释放 → 写回主存 → 其他线程获取锁 → 读取最新值
  • 因此,即使多个线程同时访问同步块,也能看到最新值并避免半初始化

3. JDK 中双重检查锁定正确写法

结合 JSR-133 和 volatile,有序且线程安全的 DCL 实现:

public class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次检查
            synchronized (Singleton.class) {    // 锁住初始化
                if (instance == null) {         // 第二次检查
                    instance = new Singleton(); // 初始化 + 写入 volatile
                }
            }
        }
        return instance;
    }
}
  • volatile:保证初始化顺序不会被重排

  • synchronized:保证多线程同时访问的互斥和可见性

  • JDK 1.5+ JVM 内存模型(JSR-133)使得双重检查锁定在现代 Java 中有效

问题 1.4 及以前 JDK 1.5+(JSR-133)
可见性 synchronized 可见性保证有限 synchronized + volatile 保证可见性
有序性 new 操作可能重排 → 半初始化 volatile + happens-before → 顺序保证
双重检查锁定 可能失败 正确可用

JSR-133 的 happens-before 语义在 JVM 层(HotSpot)实现

1. 背景:Java 内存模型与 C++ 层

  • Java 内存模型(JMM)是规范,规定了 可见性、原子性、顺序性
  • JVM 的 HotSpot 实现需要把这些语义映射到 底层硬件指令操作系统提供的同步原语
  • JVM HotSpot 是用 C++ 写的,所以所有同步行为最终由 C++ 层实现

2. happens-before 语义在 JVM 中的映射

JSR-133 中的 happens-before 规则包括:

  1. 锁释放 happens-before 同步块锁定
  2. volatile 写 happens-before 后续 volatile 读
  3. 程序顺序内的前操作 happens-before 后操作

在 C++ 层,HotSpot 通过 内存屏障(Memory Barrier / Fence)锁实现 来保证这些语义

2.1 synchronized 的实现(锁)

  • HotSpot 的 synchronized 关键字最终对应 对象头的 Monitor(Object Monitor):
    • monitorenter → 获取对象锁
    • monitorexit → 释放对象锁
// 简化示意
void JVM_MonitorExit(Monitor* mon) {
    // flush write buffer → 确保对共享变量的修改写回主存
    memory_barrier();  
    // 实际释放锁
    mon->unlock();
}

void JVM_MonitorEnter(Monitor* mon) {
    // 获取锁之前,刷新缓存 → 读取最新值
    memory_barrier();
    mon->lock();
}

效果

  • 释放锁:保证锁内写操作对其他线程可见(写屏障)
  • 获取锁:保证同步块内读取到的是主内存最新值(读屏障)

2.2 volatile 的实现

Java 层:

private volatile int x;
x = 42;       // 写
int y = x;    // 

HotSpot C++ 层:

  • 写 volatile → 使用 StoreStore + StoreLoad 内存屏障
    • StoreStore barrier:保证前面的写在写入 volatile 之前完成
    • StoreLoad barrier:保证后续读不会重排到 volatile 写之前
  • 读 volatile → 使用 LoadLoad + LoadStore barrier
    • LoadLoad barrier:保证前面的读完成
    • LoadStore barrier:保证后续写不会重排到 volatile 读之前

实际 CPU 指令:

  • x86:通常用 mfencelock 前缀
  • ARM:用 dmb / dsb 指令

2.3 总结 JVM 层映射

JMM 概念 JVM HotSpot C++ 实现 原理
synchronized 获取锁 MonitorEnter + memory_barrier() 读屏障,保证看到主内存最新值
synchronized 释放锁 MonitorExit + memory_barrier() 写屏障,保证修改写回主内存
volatile 写 StoreStore + StoreLoad barrier 保证写操作顺序,写入主内存可见
volatile 读 LoadLoad + LoadStore barrier 保证读操作顺序,读取主内存最新值
程序顺序内操作(单线程) 编译器指令顺序 + CPU 顺序保证 保证单线程内顺序性

3. 双重检查锁定(DCL)在 JVM 层安全的实现

  • 线程一执行:
    1. instance = new Singleton()
      • C++ 层会插入内存屏障,保证初始化完成后才写入 volatile 引用
  • 线程二执行:
    1. 第一次检查 if (instance == null)
      • 进入 synchronized 时,会从主内存刷新最新 instance
    2. 第二次检查 if (instance == null)
      • 读到的是完整初始化对象
  • 这样 DCL 就在硬件和 JVM 层都保证了安全