DCL(双锁检测)单例模式

本文深入探讨了单例模式在多线程环境下存在的线程安全问题,通过逐步改进代码,最终引入volatile关键字解决由指令重排序引起的对象初始化问题。详细解析了双重检查锁定(DCL)机制,并解释了volatile如何确保单例模式的正确性。

我们第一次写的单例模式是下面这样的:

public class Singleton {
    private static Singleton instance = null;

    public static Singleton getInstance() {
        if(null == instance) {                    // line A
            instance = new Singleton();        // line B
        }
        
        return instance;       
    }
}

假设这样的场景:两个线程并发调用Singleton.getInstance(),假设线程一先判断instance是否为null,即代码中line A进入到line B的位置。

刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然为空,因此线程二执行了new Singleton()操作。

片刻之后,线程一被重新唤醒,它执行的仍然是new Singleton()操作,这样问题就来了,new出了两个instance,这还能叫单例吗?


紧接着,我们再做单例模式的第二次尝试:

public class Singleton {
    private static Singleton instance = null;

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

比起第一段代码仅仅在方法中多了一个synchronized修饰符,现在可以保证不会出线程问题了。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上来说很不划算

继续把代码改成下面这样:

public class Singleton {
    private static Singleton instance = null;

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

基本上,把synchronized移动到代码内部是没有什么意义的,每次调用getInstance()还是要进行同步。同步本身没有问题,但是我们只希望在第一次创建instance实例的时候进行同步,因此有了下面的写法——双重锁定检查(DCL,Double Check Lock)

public class Singleton {
    private static Singleton instance = null;

    public  static Singleton getInstance() {
        if(null == instance) {    // 线程二检测到instance不为空
            synchronized (Singleton.class) {
                if(null == instance) {                    
                    instance = new Singleton();    // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)    
                }
            }
        }
        
        return instance;    // 后面线程二执行时将引发:对象尚未初始化错误    
    }
}

 看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个if中就返回了,因此不会走到同步块中,已经完美了吗?

  如上代码段中的注释:假设线程一执行到 instance = new Singleton() 这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

  1)给instance实例分配内存;

  2)初始化instance的构造器;

  3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)

  如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:

  a)给instance实例分配内存;

  b)将instance对象指向分配的内存空间;

  c)初始化instance的构造器;

  这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。

  具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)

  根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量

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

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

将变量instance使用volatile修饰即可实现单例模式的线程安全。

 

volatile为什么能实现禁止指令重排序呢?

请参考下面这篇博客:

https://blog.youkuaiyun.com/Rex_WUST/article/details/90143474

 

 

 

 

 

 

 

 

 

 

 

 

 

<think>我们使用双重检查锁定DCL)懒汉式单例模式来创建猪八戒对象。这种模式在需要时才创建实,并且通过双重检查锁定来保证线程安全。 步骤: 1. 构造器私有化,防止外部直接创建对象。 2. 定义一个私有的静态变量,用于存储对象(这里用volatile修饰,保证可见性和禁止指令重排序)。 3. 提供一个公共的静态方法获取实,在方法内部进行双重检查: 首先检查是否已经被创建(第一次检查),如果未创建则进入同步代码块。 在同步代码块中再次检查(第二次检查),确保在等待锁的过程中没有其他线程已经创建了实。 然后创建实。 4. 注意:在Java 5及以后版本,使用volatile可以确保DCL的正确性。 由于猪八戒只有一个,我们使用单例模式来模拟。 下面用Java代码实现: </think>### DCL懒汉式单例模式实现猪八戒对象 使用双重检查锁定DCL,Double-Checked Locking)懒汉式单例模式创建线程安全的猪八戒对象,实现步骤如下: ```java public class Bajie { // 1. 私有化构造方法(防止外部实化) private Bajie() { System.out.println("创建猪八戒实"); } // 2. volatile关键字保证可见性和有序性(JDK5+内存模型修正后有效) private static volatile Bajie instance; // 3. 双重检查锁定获取实 public static Bajie getInstance() { if (instance == null) { // 第一次检查(避免不必要的同步) synchronized (Bajie.class) { if (instance == null) { // 第二次检查(确认未初始化) instance = new Bajie(); // 安全初始化 } } } return instance; } // 4. 成员方法示 public void useSkill() { System.out.println("九齿钉耙攻击!"); } } ``` ### 代码解析 1. **线程安全**: - `synchronized` 块保证只有一个线程能初始化实 - `volatile` 修饰符阻止指令重排序(避免返回未完全初始化的对象) 2. **性能优化**: - 外层 `if (instance == null)` 避免每次访问都加锁 - 内层 `synchronized` 块确保创建操作的原子性 3. **验证**: ```java public static void main(String[] args) { // 获取猪八戒实 Bajie bajie1 = Bajie.getInstance(); Bajie bajie2 = Bajie.getInstance(); bajie1.useSkill(); // 输出:九齿钉耙攻击! System.out.println(bajie1 == bajie2); // 输出:true(确认是同一实) } ``` ### 注意事项 1. **JDK版本要求**:需JDK 5+(修复了早期内存模型缺陷) 2. **反射漏洞**:通过反射仍可破坏(需额外处理) 3. **序列化安全**:如需序列化需实现`readResolve()`方法 > 该实现是延迟初始化(lazy initialization)和线程安全的最佳平衡方案,符合单例模式的"一职责"和"全局访问点"原则[^1]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值