美文网首页
避免Double-Checked Locking

避免Double-Checked Locking

作者: YoungJadeStone | 来源:发表于2019-04-26 14:17 被阅读0次

    Double-Checked Locking是一个广泛使用于多线程环境下lazy initialization的实现方法。但实际上在Java里,由于Java的内存环境和编译器,它并没有真正的实现我们期望的功能。

    1. 什么是Double-Checked Locking

    首先我们来看一下我们希望的功能。在单线程时:

    // Single threaded version
    class Foo { 
      private Helper helper = null;
      public Helper getHelper() {
        if (helper == null) 
            helper = new Helper();
        return helper;
      }
      // other functions and members...
    }
    

    我们希望如果helper是null的时候,我们创建一个它的instance。如果不是,我们直接使用。我们并不总是给helper赋个值,这也是为什么叫lazy initialization。

    但是在多线程的时候,上面的代码并不工作。因为可能有两个线程同时访问helper。比如开始的时候helpernull,线程A检查if (helper == null),进入赋值语句,但是还没赋值的时候,线程B也开始检查if (helper == null),哪怕接下来线程A已经完成了helper的赋值,线程B也还是会再进行一遍。

    如果简单的用synchronize,我们会有下面的一段代码:

    // Correct multithreaded version
    class Foo { 
      private Helper helper = null;
      public synchronized Helper getHelper() {
        if (helper == null) 
            helper = new Helper();
        return helper;
      }
      // other functions and members...
    }
    

    synchronized能保证不同线程在读取getHelper的时候是线程安全的。这是synchronized method的概念。但是我们每次调用getHelper的时候,都会用到synchronization,很浪费资源。我们可以采用两次检测helper是否为null的方式来避免过度的使用synchronization。

    // Broken multithreaded version
    // "Double-Checked Locking" idiom
    class Foo { 
      private Helper helper = null;
      public Helper getHelper() {
        if (helper == null) {
          synchronized(this) {
            if (helper == null) {
              helper = new Helper();
            }
          }    
        }
        return helper;
      }
      // other functions and members...
    }
    

    上面这种方法就是Double-Checked Locking。

    2. 为什么Double-Checked Locking不工作

    很遗憾的是,Double-Checked Locking实际上在Java里并没有按预期工作。实际上,如果不能让每个access到helper这个object的线程执行synchronization,是没办法让这个代码工作的。

    2.1 不工作的第一个原因

    最显著的原因就是对helper的初始化指令和写指令可能会被重排序。当其他线程再次调用getHelper的时候,会得到一个没有被初始化完成的helper对象。

    比如下面这段代码(出自博文):

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

    我们来分析一下这个过程(同样出自博文):

    对于JVM而言,它执行的是一个个Java指令。在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。(即先赋值指向了内存地址,再初始化)这样就使出错成为了可能,我们仍然以A、B两个线程为例:

    • A、B线程同时进入了第一个if判断
    • A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
    • 由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
    • B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

    此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。”

    2.2 无效的修复

    下面这个例子是一个看似有用,实则无效的修复:

    // (Still) Broken multithreaded version
    // "Double-Checked Locking" idiom
    class Foo { 
      private Helper helper = null;
      public Helper getHelper() {
        if (helper == null) {
          Helper h;
          synchronized(this) {
            h = helper;
            if (h == null) 
                synchronized (this) {
                  h = new Helper();
                } // release inner synchronization lock
            helper = h;
            } 
          }    
        return helper;
        }
      // other functions and members...
    }
    

    因为synchronization的规则保证我们能在退出内部同步块之前Helper能够被实例化,h能够被赋值,但是不能保证Helper被赋值一定发生在退出同步块之后,所以还是有可能没有被初始化的Helper实例被其他线程引用/访问。

    3. 解决方法

    3.1 使用static singletons

    有一个最简单而有效地方法就是创建一个static singleton。这样就只有一个Helper被创建。

    class HelperSingleton {
      static Helper singleton = new Helper();
    }
    

    这个singleton在被引用之前不会被初始化,而一次初始化之后也不会再被重复初始化。

    3.2 如果是32位原始类型,不需要考虑修复

    对于32-bit primitive values(比如int, float),double-checked locking是能正常工作的。但是对于64-bit primitive values(比如long, double)就不行。因为对于64-bit primitive的unsynchronized的读写操作是不保证原子性的。

    实际上,我们还能进一步简略。在下面代码中,如果computeHashCode()总是返回相同值(没有副作用),那么我们甚至可以省略掉synchronized

    // Lazy initialization 32-bit primitives
    // Thread-safe if computeHashCode is idempotent
    class Foo { 
      private int cachedHashCode = 0;
      public int hashCode() {
        int h = cachedHashCode;
        if (h == 0) {
          h = computeHashCode();
          cachedHashCode = h;
        }
        return h;
      }
      // other functions and members...
    }
    

    3.3 用Thread Local Storage修复

    让每一个线程都保持一个flag,这个flag用来决定这个线程有没有完成需要的synchronization。

    class Foo {
      /** If perThreadInstance.get() returns a non-null value, 
      this thread has done synchronization needed to 
      see initialization of helper */
      private final ThreadLocal perThreadInstance = new ThreadLocal();
      private Helper helper = null;
      public Helper getHelper() {
        if (perThreadInstance.get() == null) createHelper();
        return helper;
      }
      private final void createHelper() {
        synchronized(this) {
          if (helper == null)
            helper = new Helper();
          }
          // Any non-null value would do as the argument here
          perThreadInstance.set(perThreadInstance);
      }
    }
    

    这种方法很依赖于JDK的实现。在Sun 1.2的实现版本中,ThreadLocal很慢,而1.3就有显著提升。

    3.4 使用Volatile字节 (JDK5及以上)

    在JDK5及以上,如果一个field被标为Volatile,系统对它的读或写的重排序有了更多的规定。(更详细的资料

    所以我们可以通过对helpervolatile的方式来解决问题:

    // Works with acquire/release semantics for volatile
    // Broken under current semantics for volatile
    class Foo {
      private volatile Helper helper = null;
      public Helper getHelper() {
        if (helper == null) {
          synchronized(this) {
            if (helper == null)
              helper = new Helper();
            }
          }
          return helper;
        }
    }
    

    3.5 使用不变实例 (JDK5及以上)

    如果helper是一个immutable object,比如Helper这个类型的所有field都是final的,那么不需要标记Volatile也可以让double-checked locking工作。因为我们对immutable object的读写操作都是atomic的。


    Reference

    相关文章

      网友评论

          本文标题:避免Double-Checked Locking

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