美文网首页JavaJava多线程专题
[Java多线程编程之十三] DCL缺陷与优化

[Java多线程编程之十三] DCL缺陷与优化

作者: 小胡_鸭 | 来源:发表于2020-12-20 14:34 被阅读0次

一、DCL问题分析

  DCL,即Double Check Lock,双重检查锁定,通常使用在懒加载的单例模式中,一般单例模式里的懒加载代码如下:

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

  在单线程中,该单例模式是有效的,但是在多线程程序中,对于这种先检查后操作的先验条件,如果没有同步策略,会产生竞态条件,最终导致线程安全问题,可能导致重复创建了多次对象。

  为了保证代码同步,可以将 getInstance() 方法声明为 synchronized,但是这会导致不管单例是否已经实例化,每次获取单例都要加锁加锁,性能底下,所以应该用同步代码块包裹if分支里的代码,并且为了避免单例被重复创建,在获取锁之后还要再检查一次单例是否为空来决定是否执行创建单例对象的操作,代码如下所示:

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;
    }
}

  这个写法兼顾了效率和安全性:

  • 如果 instance 为空,直接返回单例;
  • 如果 instance 不为空,为了避免多个线程重复创建对象,使用 synchronized 同步;
  • 第一个拿到锁的线程,判断到单例为空,创建对象,然后释放锁,而后续的对象拿到锁后判断单例非空,退出同步块,返回单例,对于再晚一些的线程(第一个线程完成构建对象之后的线程),到了第一个判断到单例非空,也直接返回单例。

  看似非常完美,实际上依然存在问题,对象的创建实际上分为三个部分:

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

  但是,由于可能发生重排序,2跟3的执行顺序可能会反过来,如下:

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

  如果发生这种情况,会导致第一个线程完成构建对象之后的进入方法的线程在第一个判断时可能判断到单例非空,但是该单例还没有完成初始化,就被返回了,没有被安全发布,当外部程序使用了这个未被正确初始化的单例时,会发生不可预测的错误。

二、解决方案

  为了解决上述问题,核心是保证单例被正确地初始化,解决办法有:
1、不允许创建单例对象时2和3的重排序
2、允许2和3的重排序,但是在对象被正确构建完成之前,其他线程不允许看到这个“重排序”

2.1 基于volatile的解决方案

  volatile可以保证共享变量的可见性,还能禁止非线程安全的重排序,所以将单例声明为 volatile 可以避免上述的重排序,代码如下:

public class Singleton {

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

2.2 基于类初始化的解决方案

  类加载器在加载类时会初始化被声明为 static 的变量和代码块,JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化,代码如下:

public class Singleton {
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

  为了满足懒加载,不能直接在类中直接定义一个声明为 static 的实例,所以要定义一个内部静态类的静态加载来满足这种需求,JVM可以在使用类的线程之前,完成这种静态初始化,因此该方案的本质是运行步骤2和步骤3重排序,但是不允许其他线程看见。

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

相关文章

  • [Java多线程编程之十三] DCL缺陷与优化

    一、DCL问题分析   DCL,即Double Check Lock,双重检查锁定,通常使用在懒加载的单例模式中,...

  • DCL缺陷和优化

    DCL的问题 单利模式是我们经常用到的一种模式,但是要正确的书写和理解一个单利模式却没有那么简单,首先我们看看下面...

  • Java多线程目录

    Java多线程目录 Java多线程1 线程基础Java多线程2 多个线程之间共享数据Java多线程3 原子性操作类...

  • 带你搞懂Java多线程(四)

    带你搞懂Java多线程(一)带你搞懂Java多线程(二)带你搞懂Java多线程(三) 什么是线程间的协作 线程之间...

  • 多线程--基础

    Java多线程 从本篇开始,笔者开始了一个新的专题,来说说Java多线程。 在讲解Java多线程之前,我们来了解下...

  • Java多线程

    Java多线程 从本篇开始,笔者开始了一个新的专题,来说说Java多线程。 在讲解Java多线程之前,我们来了解下...

  • iOS多线程之NSOperations

    相关文章:iOS多线程之NSThreadiOS多线程之GCD NSOperation(任务)与NSOperatio...

  • Java多线程学习:Future、Callable

    Java多线程编程:Callable、Future和FutureTask浅析(多线程编程之四) 最近在写清结算文件...

  • iOS多线程相关面试题

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

  • 多线程之--NSOperation

    iOS多线程demo iOS多线程之--NSThread iOS多线程之--GCD详解 iOS多线程之--NSOp...

网友评论

    本文标题:[Java多线程编程之十三] DCL缺陷与优化

    本文链接:https://www.haomeiwen.com/subject/aeahnktx.html