好的,要深入理解DCL(Double-Checked Locking)双重校验锁的原理、问题以及解决方法。 首先,我需要回忆一下单例模式的基本概念,因为DCL通常用于实现单例模式。 单例模式确保一个类只有一个实例,并提供一个全局访问点。 接下来,DCL的目的是为了减少同步的开销。通常,如果直接在getInstance方法上加synchronized关键字, 虽然能保证线程安全,但每次获取实例时都需要同步,这在多线程环境下可能会影响性能。 而DCL通过两次检查实例是否已经创建,来避免每次调用都进行同步,只在第一次创建实例的时候同步。 现在,我需要想一下DCL的标准写法。大概是这样的: public class Singleton { private static Singleton instance; // 必须使用 volatile private Singleton() {} // 私有构造函数 public static Singleton getInstance() { if (instance == null) { // 第一次检查(避免不必要的同步) synchronized (Singleton.class) { // 同步块 if (instance == null) { // 第二次检查(确保唯一实例) instance = new Singleton(); // 实例化 } } } return instance; } } 不过,这种写法在早期的Java中存在问题,主要涉及到指令重排序的问题。 因为instance = new Singleton(); 这个操作并不是原子性的,它分为三个步骤: 1. 分配内存空间 2. 初始化对象 3. 将instance指向分配的内存地址 但由于指令重排序的可能,步骤2和步骤3可能会被颠倒,导致其他线程在第一次检查时看到instance不为null,但实际上对象还没有初始化完成,从而访问到一个不完整的对象。 为了解决这个问题,Java 5之后引入了volatile关键字,可以禁止指令重排序, 确保写操作之前的操作不会被重排序到写操作之后。因此,正确的DCL实现需要将instance声明为volatile: private static volatile Singleton instance; 这样,使用volatile修饰instance变量,就能保证在instance被初始化完成之后, 其他线程才能看到它的值,从而避免了指令重排序带来的问题。 不过,需要确认一下volatile在Java内存模型中的具体作用。volatile关键字主要有两个作用: 1. 可见性:保证变量的修改对所有线程立即可见。 2. 禁止指令重排序:通过插入内存屏障,确保编译器和处理器不会对指令进行重排序。 在DCL中,主要是利用了volatile的第二个特性,即禁止指令重排序,确保对象的初始化在赋值操作之后完成。 另外,我还需要思考其他实现单例模式的方式, 比如静态内部类的方式,或者枚举方式,这些方式是否更简单且线程安全。 静态内部类的实现方式: public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; // 类加载时初始化,由 JVM 保证线程安全 } } 这种方式利用了类加载机制,保证在首次访问Holder.INSTANCE时才会加载Holder类, 从而初始化Singleton实例,由JVM保证线程安全。 这种方法不需要显式的同步,且实现简单,但可能无法应对反射攻击或者序列化的问题。 枚举方式: public enum Singleton { INSTANCE; } 这种方式由JVM保证实例的唯一性和线程安全,且能防止反射和序列化破坏单例,是最推荐的方式, 但可能不够灵活,比如无法继承其他类。 回到DCL,虽然它在某些情况下仍然有用,但需要正确使用volatile关键字。 还需要验证一下如果不使用volatile是否一定会出现问题,以及这种问题发生的条件。 比如,在Java 5之前,DCL确实是不安全的,因为volatile关键字的语义不够严格, 无法完全禁止指令重排序。而在Java 5之后,JSR-133增强了volatile的内存语义,确保了DCL的正确性。 另外,我还需要了解内存屏障的概念,以及volatile如何通过内存屏障来禁止重排序。 比如,在写操作之后插入StoreStore屏障,防止写操作之前的指令被重排序到写操作之后; 在读操作之前插入LoadLoad屏障,防止读操作之后的指令被重排序到读操作之前。 总结一下,DCL的正确实现需要注意以下几点: 1. 使用双重检查来减少同步的开销。 2. 使用volatile关键字来禁止指令重排序,保证实例的可见性和初始化顺序。 3. 确保构造函数是私有的,防止外部实例化。 可能的误区是认为只要两次检查就能保证线程安全,而忽略了指令重排序的问题。 因此,volatile的使用是必不可少的。 最后,我需要思考在实际开发中,是否应该优先选择其他单例实现方式, 如枚举或静态内部类,而不是DCL,以避免潜在的复杂性和错误。 但在某些需要延迟初始化且对性能有较高要求的场景下,DCL仍然是一个合理的选择,只要正确实现。
前言:什么是单例模式
单例即单实例,只实例出来一个对象。
一般在创建一些管理器类、工具类的时候,需要用到单例模式,比如JDBCUtil 类,我们只需要一个实例即可(多个实例也可以实现功能,但是增加了代码量且降低了性能)。
如何实现单例:
- 将构造方法私有化
- 提供一个全局唯一获取该类实例的方法帮助用户获取类的实例
应用场景:
主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。
单例模式的优点:
单例模式为系统资源的优化提供了很好的思路,频繁创建和销毁对象都会增加系统的资源消耗,而单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。
单例模式的四类写法:
- 饿汉模式
- 懒汉模式
- 静态内部类
- 双重校验锁
在讲双重校验锁之前先来看一下其他模式
饿汉模式
顾名思义,饿汉模式就是加载类的时候直接new一个对象,后面直接用即可。
饿汉模式指在类中直接定义全局的静态对象的实例并初始化,然后提供一个方法获取该实例对象。