杰瑞科技汇

Java synchronized 单例如何优化性能?

为什么需要线程安全的单例?

在单线程环境下,一个简单的单例模式就能完美工作,但在多线程环境下,如果多个线程同时调用单例的获取方法(通常是 getInstance()),可能会创建出多个实例,这就破坏了单例的唯一性。

问题场景: 假设一个非线程安全的单例类:

public class Singleton {
    private static Singleton instance;
    private Singleton() {} // 私有构造函数
    public static Singleton getInstance() {
        if (instance == null) { // 线程A和线程B可能同时在这里判断为true
            instance = new Singleton(); // 然后各自创建一个实例
        }
        return instance;
    }
}

当线程A执行到 if (instance == null) 时,它判断为 true,但还没来得及执行 instance = new Singleton(),此时CPU时间片用完,线程B开始执行,线程B同样判断 if (instance == null),也为 true,于是线程B也创建了一个新的实例,这样就产生了两个不同的实例,单例模式失效。

synchronized 解决方案

为了解决上述问题,最直接的想法就是给 getInstance() 方法加上 synchronized 关键字,确保同一时间只有一个线程能进入这个方法。

同步整个方法(简单但低效)

这是最原始的 synchronized 单例实现。

public class Singleton {
    private static Singleton instance;
    private Singleton() {
        // 防止外部通过反射创建实例
        if (instance != null) {
            throw new RuntimeException("Use getInstance() method to get the single instance.");
        }
    }
    // 整个方法被 synchronized 修饰
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

工作原理:

Java synchronized 单例如何优化性能?-图1

  • synchronized 关键字确保了在任何时刻,只有一个线程可以执行 getInstance() 方法。
  • 当线程A进入该方法并创建实例后,线程B、C等再想调用 getInstance() 时,必须等待线程A执行完毕。
  • 这样,instance == null 的判断和实例的创建过程是原子性的,避免了重复创建的问题。

优点:

  • 实现简单,易于理解。
  • 线程安全,能保证单例的唯一性。

缺点:

  • 性能开销大,无论实例是否已经创建,每次调用 getInstance() 都需要进行同步,同步操作(获取锁、释放锁)是比较耗时的,这会严重影响性能,尤其是在高并发场景下,当单例对象已经创建后,后续的调用仍然在同步等待,这是不必要的。

同步代码块(优化版)

为了解决方案一的性能问题,我们可以只同步“创建实例”这一小段代码,而不是整个方法。

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

这种写法被称为 “双重检查锁定”(Double-Checked Locking, DCL)

工作原理:

Java synchronized 单例如何优化性能?-图2

  1. 第一次检查 (if (instance == null))
    • 大多数情况下,单例实例已经存在,线程调用 getInstance() 时,直接通过第一个 if 判断,返回实例,完全不需要进入同步块,性能很高。
  2. 进入同步块 (synchronized (Singleton.class))
    • 只有当 instancenull 时,线程才会尝试获取锁,这大大减少了同步的开销。
  3. 第二次检查 (if (instance == null))
    • 当线程A获取锁进入同步块后,可能会发生线程B在队列中等待,当线程A执行完毕释放锁后,线程B进入同步块,线程B需要再次检查 instance 是否为 null,因为线程A可能已经创建了实例,如果线程B不检查,它也会再次创建一个实例,导致重复创建。

优点:

  • 性能高,只有在第一次创建实例时需要同步,后续调用几乎是无锁的,性能接近于非线程安全的版本。
  • 线程安全,通过双重检查,确保了只有一个实例被创建。

必须使用 volatile 关键字! 这是一个至关重要的点。在 Java 1.4 及更早版本中,DCL 是有缺陷的,不能正常工作。 从 Java 5 开始,volatile 关键字的语义得到了增强,可以解决这个问题。

为什么需要 volatile 问题出在 instance = new Singleton(); 这行代码,它并非一个原子操作,大致可以分解为三个步骤:

  1. 分配对象的内存空间。
  2. 初始化对象。
  3. instance 引用指向分配的内存地址。

在多线程环境下,由于指令重排序,执行顺序可能是 1 -> 3 -> 2。

  • 线程A 执行了 1 和 3,但还没执行 2。instance 已经不为 null 了,但它是一个未完全初始化的对象。
  • 线程B 调用 getInstance(),进行第一次检查 if (instance == null),由于 instance 已经指向了内存地址(步骤3已完成),判断为 false,于是直接返回了这个未初始化的 instance
  • 线程B 得到了一个不完整的、有问题的对象,导致程序出错。

volatile 关键字可以禁止指令重排序,确保 instance = new Singleton(); 的三个步骤按 1->2->3 的顺序完成,从而避免了上述问题。

Java synchronized 单例如何优化性能?-图3


其他更优的单例实现方式

虽然 synchronized 是经典实现,但在现代 Java 开发中,有更简洁、更高效的单例模式实现方式。

静态内部类(推荐)

这种方法利用了 JVM 类加载机制来保证线程安全,是推荐使用的方式之一。

public class Singleton {
    // 私有构造函数
    private Singleton() {}
    // 静态内部类
    private static class SingletonHolder {
        // 静态实例,由 JVM 在加载 SingletonHolder 类时初始化
        private static final Singleton INSTANCE = new Singleton();
    }
    // 提供公共的静态获取方法
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

工作原理:

  • Singleton 类的加载和初始化并不会立即加载 SingletonHolder 类。
  • 只有当 getInstance() 方法第一次被调用时,JVM 才会去加载 SingletonHolder 类,并初始化其静态变量 INSTANCE
  • JVM 的类加载机制是线程安全的,INSTANCE 的初始化过程也是线程安全的。
  • 这种方式既保证了线程安全,又没有带来任何同步开销,是一种完美的实现。

枚举(最推荐)

这是由 Effective Java 的作者 Joshua Bloch 提出的“最佳单例实现方式”,它不仅能避免多线程同步问题,还能防止通过反射和序列化/反序列化破坏单例。

public enum Singleton {
    INSTANCE; // 单个枚举元素就是单例实例
    // 可以添加自己的方法
    public void doSomething() {
        System.out.println("Singleton is doing something.");
    }
}

使用方式: Singleton singleton = Singleton.INSTANCE;

优点:

  • 绝对线程安全,由 JVM 保证。
  • 防止反射攻击,通过反射试图调用私有构造函数来创建枚举实例时,JVM 会直接抛出异常。
  • 防止序列化破坏,枚举类型的序列化和反序列化是由 JVM 保证的,它总能保证反序列化后的对象和序列化前是同一个对象。

总结与对比

实现方式 线程安全 性能 简洁性 防止反射/序列化破坏 推荐度
synchronized 同步方法 简单 ⭐⭐
synchronized 双重检查 是(需volatile 较复杂 ⭐⭐⭐
静态内部类 极高 简洁 ⭐⭐⭐⭐
枚举 极高 最简洁 ⭐⭐⭐⭐⭐
  • 学习和理解synchronized 双重检查(DCL)是一个非常重要的多线程编程模式,理解其工作原理和 volatile

分享:
扫描分享到社交APP
上一篇
下一篇