• 双重检查锁定


    看 "java并发编程的艺术" 第3.8

    双重检查锁定与延迟初始化

      在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。双
    重检查锁定是常见的延迟初始化技术,但它是一个错误的用法。本文将分析双重检查锁定的
    错误根源,以及两种线程安全的延迟初始化方案。

    需要注意的是, 双重检查锁定本身是错误的!

    双重检查锁定的由来

      在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些
    对象时才进行初始化。此时,程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初
    始化需要一些技巧,否则很容易出现问题。比如,下面是非线程安全的延迟初始化对象的示例
    代码。

    public class UnsafeLazyInitialization {
        private static Instance instance;
    
        public static Instance getInstance() {
            if (instance == null) { // 1:A线程执行
                instance = new Instance(); // 2:B线程执行
            }
    
            return instance;
        }
    }
    

      

      在UnsafeLazyInitialization类中,假设A线程执行代码1的同时,B线程执行代码2。此时,线
    程A可能会看到instance引用的对象还没有完成初始化(出现这种情况的原因见3.8.2节)。
      对于UnsafeLazyInitialization类,我们可以对getInstance()方法做同步处理来实现线程安全
    的延迟初始化。示例代码如下。

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

      

      由于对getInstance()方法做了同步处理,synchronized将导致性能开销。如果getInstance()方
    法被多个线程频繁的调用,将会导致程序执行性能的下降。反之,如果getInstance()方法不会被
    多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。
      在早期的JVM中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销。因此,
    人们想出了一个“聪明”的技巧:双重检查锁定(Double-Checked Locking)。人们想通过双重检查
    锁定来降低同步的开销。下面是使用双重检查锁定来实现延迟初始化的示例代码。

    public class DoubleCheckedLocking { // 1
    
        private static Instance instance; // 2
    
        public static Instance getInstance() { // 3
    
            if (instance == null) { // 4:第一次检查
    
                synchronized (DoubleCheckedLocking.class) { // 5:加锁
    
                    if (instance == null) { // 6:第二次检查
                        instance = new Instance(); // 7:问题的根源出在这里
                    }
                } // 8
            } // 9
    
            return instance; // 10
        } // 11
    }
    

      

      如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始
    化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全
    其美。
      多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
    ·在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
    双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读
    取到instance不为null时,instance引用的对象有可能还没有完成初始化。

    3.8.2 问题的根源

    前面的双重检查锁定示例代码的第7行(instance=new Singleton();)创建了一个对象。这一
    行代码可以分解为如下的3行伪代码。

    memory = allocate();  // 1:分配对象的内存空间
    ctorInstance(memory); // 2:初始化对象
    instance = memory;  // 3:设置instance指向刚分配的内存地址

    上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实
    发生的,详情见参考文献1的“Out-of-order writes”部分)。2和3之间重排序之后的执行时序如
    下。

    memory = allocate();  // 1:分配对象的内存空间
    instance = memory;  // 3:设置instance指向刚分配的内存地址
    // 注意,此时对象还没有被初始化!
    ctorInstance(memory); // 2:初始化对象
    

      

      上面3行伪代码的2和3之间虽然被重排序了,但这个重排序
    并不会违反intra-thread semantics。这个重排序在没有改变单线程程序执行结果的前提下,可以
    提高程序的执行性能。
    为了更好地理解intra-thread semantics,请看如图3-37所示的示意图(假设一个线程A在构
    造对象后,立即访问这个对象)。
    如图3-37所示,只要保证2排在4的前面,即使2和3之间重排序了,也不会违反intra-thread
    semantics。
    下面,再让我们查看多线程并发执行的情况。如图

  • 相关阅读:
    数组
    2022杭电多校部分好题题解(更新至Day 10)
    js入门
    JS数据类型转换
    python赚钱小秘籍每日打新提醒
    jenkins+github+python执行测试用例
    python网络编程socket基础
    Dockerfile介绍与实操
    Docker部署jenkins详细过程
    Faker生成范围内的随机时间格式
  • 原文地址:https://www.cnblogs.com/jiuya/p/10792466.html
Copyright © 2020-2023  润新知