杰瑞科技汇

Java单例synchronized如何高效保证线程安全?

Java单例模式深度剖析:Synchronized的“正确打开方式”与性能优化之路

** 本文将深入探讨Java单例模式的核心实现,并聚焦于如何使用synchronized关键字保证线程安全,我们将从最基础的实现开始,逐步分析其潜在问题,并最终引出性能更优的解决方案,帮助你在面试和实际开发中游刃有余。

Java单例synchronized如何高效保证线程安全?-图1
(图片来源网络,侵删)

引言:为什么单例模式如此重要?

在软件开发中,单例模式(Singleton Pattern)是最简单、最常用的设计模式之一,它的核心目标是确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。

想象一下:

  • 配置管理器: 整个应用程序只需要一份配置信息。
  • 数据库连接池: 避免频繁创建和销毁连接带来的性能开销。
  • 日志记录器: 保证所有日志都写入同一个文件或控制台。

如果这些对象被多次实例化,不仅会浪费资源,还可能导致数据不一致的严重问题,掌握如何正确、高效地实现单例模式,是每一位Java程序员必备的核心技能,而synchronized,正是实现线程安全单例的“敲门砖”。


第一部分:初识单例模式与Synchronized

1 懒汉式:非线程安全的“雏形”

最直观的单例实现是“懒汉式”,即只有在第一次使用时才创建实例。

Java单例synchronized如何高效保证线程安全?-图2
(图片来源网络,侵删)
public class LazySingleton {
    private static LazySingleton instance;
    // 私有构造函数,防止外部 new
    private LazySingleton() {}
    // 全局访问点
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton(); // 第一次调用时创建
        }
        return instance;
    }
}

问题所在: 在多线程环境下,如果两个线程同时调用getInstance()方法,并且都通过了instance == null的判断,那么就会创建两个LazySingleton实例,这严重破坏了单例的原则,是线程不安全的。

2 懒汉式升级:Synchronized的“第一次”尝试

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

public class SynchronizedSingleton {
    private static SynchronizedSingleton instance;
    private SynchronizedSingleton() {}
    // 使用 synchronized 关键字保证线程安全
    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

优点:

  • 线程安全: synchronized确保了在任何时刻,只有一个线程可以执行getInstance()方法,完美解决了多线程下创建多个实例的问题。

致命缺点:

Java单例synchronized如何高效保证线程安全?-图3
(图片来源网络,侵删)
  • 性能低下: 这是synchronized在方法级别上锁的典型问题,即使实例已经被创建,后续所有线程在调用getInstance()时,都必须在方法入口处等待锁,这导致每次获取实例都变成了一个同步操作,极大地影响了性能,尤其是在高并发场景下。

第二部分:Synchronized的“精妙”之处与优化

既然直接锁整个方法性能太差,我们能不能只锁“创建实例”这一行代码呢?这就引出了我们优化的核心方向。

1 双重检查锁定:DCL(Double-Checked Locking)

“双重检查锁定”模式是synchronized应用的一个经典案例,它旨在既保证线程安全,又避免不必要的同步开销。

public class DCLSingleton {
    // volatile 是关键,防止指令重排序
    private static volatile DCLSingleton instance;
    private DCLSingleton() {}
    public static DCLSingleton getInstance() {
        // 第一次检查(无锁)
        if (instance == null) {
            // 加锁,确保只有一个线程能进入此代码块
            synchronized (DCLSingleton.class) {
                // 第二次检查(有锁)
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

代码解析:

  1. 第一次检查 (if (instance == null)):

    • synchronized块之外进行,当实例已经被创建后,所有线程都可以直接获取到instance,而无需进入同步块,大大提升了性能。
  2. synchronized 块:

    • 只有当instancenull时,线程才会尝试获取锁,这避免了每次调用都同步的问题。
  3. 第二次检查 (if (instance == null)):

    • synchronized块内部再次检查,这是为了防止在等待锁的过程中,另一个线程已经创建了实例,如果没有第二次检查,当线程A获取锁后,可能会重复创建实例。

2 为什么必须使用 volatile

这是DCL模式中最容易被忽略,但至关重要的一点。instance = new DCLSingleton(); 这行代码在JVM中并非一个原子操作,大致可以分解为三步:

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

如果没有volatile,由于指令重排序,执行顺序可能变成 1 -> 3 -> 2,这就导致了灾难性的后果:

  • 线程A 进入synchronized块,执行了步骤1和3,但还没来得及执行步骤2(初始化)。
  • instance 已经不为 null 了。
  • 线程B 调用getInstance(),第一次检查 instance != null,直接返回了一个未初始化完成的对象,如果此时线程B访问这个对象的任何字段,都可能导致 NullPointerException 或其他不可预知的错误。

volatile 关键字有两个作用:

  1. 保证可见性: 一个线程修改了volatile变量,新值会立刻同步到主内存,并且其他线程读取时会从主内存读取,保证了线程间的可见性。
  2. 禁止指令重排序: 它会插入一个“内存屏障”,确保instance = new DCLSingleton()的三个步骤不会被重排序,必须按 1->2->3 的顺序执行。

第三部分:超越Synchronized——更优雅的单例实现

虽然DCL模式已经很优秀,但在Java 5之后,我们有了更简洁、更可靠的实现方式。

1 饿汉式:简单但不够灵活

public class EagerSingleton {
    // 类加载时就创建实例,由JVM保证线程安全
    private static final EagerSingleton instance = new EagerSingleton();
    private EagerSingleton() {}
    public static EagerSingleton getInstance() {
        return instance;
    }
}

优点:

  • 实现简单: 代码量少,没有锁。
  • 线程安全: 实例由JVM在类加载阶段创建,static变量在JVM中是原子操作,天然线程安全。

缺点:

  • 启动慢: 无论是否使用,类加载时就会创建实例,可能造成资源浪费。
  • 不灵活: 无法传递参数来决定如何创建实例。

2 静态内部类(推荐):完美的解决方案

这种方式被称为“ Initialization-on-demand holder idiom”,是《Effective Java》中推荐的单例模式。

public class HolderSingleton {
    // 私有构造函数
    private HolderSingleton() {}
    // 静态内部类
    private static class SingletonHolder {
        // 静态初始化器,由JVM保证线程安全
        private static final HolderSingleton INSTANCE = new HolderSingleton();
    }
    // 全局访问点
    public static HolderSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

工作原理:

  • HolderSingleton 类加载时,并不会立即加载 SingletonHolder 内部类。
  • 只有当第一次调用 getInstance() 方法时,JVM才会去加载 SingletonHolder 类。
  • 在加载 SingletonHolder 类时,其静态变量 INSTANCE 会被初始化,从而创建 HolderSingleton 的唯一实例。
  • 由于类的加载和静态变量的初始化过程由JVM保证线程安全,因此这种方式既保证了线程安全,又实现了延迟加载,还避免了synchronized带来的性能开销。

优点:

  • 线程安全: 由JVM保证。
  • 延迟加载: 真正实现了按需创建。
  • 高性能: 没有使用任何锁,获取实例的效率最高。

总结与对比

实现方式 线程安全 延迟加载 性能 备注
**懒汉式(无锁)
分享:
扫描分享到社交APP
上一篇
下一篇