DCL单例模式,如何解决DCL问题

本文深入探讨了DCL(Double Check Locking)在Java中实现线程安全的Singleton模式的方法和潜在问题。详细分析了DCL如何提高性能,以及由于JVM编译器优化可能导致的重排序问题,最后给出了使用volatile关键字和类初始化两种解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

何为DCL,DCL即Double Check Lock,双重检查锁定。下面从几个单例模式来讲解

懒汉式

public void Singleton{
    private static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(singleton==null){
               singleton=new Singleton();
        }

            return singleton;
    }

        

}

这种方法在单线程下是可取的,但是在并发也就是在多线程的情况下是不可取的,因为其无法保证线程安全,优化如下:

public void Singleton{
    private static Singleton singleton;

    private Singleton(){}

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

        return singleton;
    }

}

优化非常简单,在getInstance方法上加上了synchronized同步,尽管jdk6以后对synchronized做了优化,但还是会效率较低的,性能下降。那该如何解决这个问题?于是有人就想到了双重检查DCL

public void Singleton{
    private static Singleton singleton;

    private Singleton(){}

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

}

这个代码看起来perfect:

  1. 如果检查第一一个singleton不为null,则不需要执行加锁动作,极大的提高了性能
  2. 如果第一个singleton为null,即使有多个线程同时判断,但是由于synchronized的存在,只有一个线程能创建对象
  3. 当第一个获取锁的线程创建完成singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象

DCL看起来非常完美,但其实这个是不正确的。逻辑没问题,分析也没问题?但为何是不正确的?不妨我们先回顾一下创建对象的过程

  1. 为对象分配内存空间
  2. 初始化对象
  3. 将内存空间的地址赋值给对应的引用

但由于jvm编译器的优化产生的重排序缘故,步骤2、3可能会发生重排序:

  1. 为对象分配内存空间
  2. 将内存空间的地址赋值给对应的引用
  3. 初始化对象

如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象

知道问题的原因,那么我们就可以解决?

不允许重排序

重排序不让其他线程看到

解决方法

利用volatile的特性即可阻止重排序和可见性

public class Singleton {
   //通过volatile关键字来确保安全
   private volatile static Singleton singleton;

   private Singleton(){}

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

类初始化的解决方案

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

   public static Singleton getInstance(){
       return SingletonHolder.singleton;
   }
}

该解决方案的根本就在于:利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

Java语言规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之相对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。

 

### 单例模式中的双重检查锁定(DCL)实现 在单例模式中,双重检查锁定(Double-Checked Locking, DCL)是一种常见的优化技术,用于确保线程安全的同时避免不必要的同步开销。以下是基于引用内容和专业知识对DCL实现的详细说明。 #### 1. 基本实现 双重检查锁定的核心思想是在访问实时进行两次非空检查,以确保线程安全并减少锁的使用频率。以下是一个典型的DCL实现[^2]: ```java package com.geek.singleton.lazy.dcl; public class Singleton { private static volatile Singleton instance; // 使用volatile关键字 private Singleton() {} // 私有构造函数 public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { // 加锁 if (instance == null) { // 第二次检查 instance = new Singleton(); // 创建实 } } } return instance; } } ``` #### 2. 关键点解析 - **`volatile`关键字**: 在上述代码中,`volatile`关键字被用来修饰`instance`变量,以确保多线程环境下的可见性和有序性。这是因为`new`对象的操作并非原子性的,可能会导致指令重排序问题,从而使其他线程看到一个未完全初始化的对象[^2]。 - **两次非空检查**: 第一次检查是为了避免不必要的同步操作,提高性能;第二次检查是在加锁后再次确认实是否已经被创建,从而确保线程安全。 - **`synchronized`块**: 只有在`instance`为`null`时才会进入同步块,因此减少了锁的竞争,提升了效率。 #### 3. 工作原理 当多个线程同时调用`getInstance()`方法时,可能会发生以下情况: - 如果`instance`已经存在,则直接返回实,无需进入同步块。 - 如果`instance`尚未创建,则通过同步块确保只有一个线程能够创建实,其余线程等待或直接获取已创建的实。 #### 4. 注意事项 - **JVM内存模型**: `volatile`关键字确保了`instance`的写入操作对所有线程可见,并禁止指令重排序,从而避免了部分初始化的问题[^2]。 - **线程安全性**: DCL模式在Java 5及以上版本中是线程安全的,因为`volatile`关键字的行为在JMM(Java Memory Model)中得到了明确规定。 #### 示测试 以下是一个简的测试代码,验证DCL实现的正确性: ```java public class DCLTest { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); Singleton instance1 = Singleton.getInstance(); System.out.println(instance == instance1); // 输出 true } } ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值