Skip to content

Latest commit

 

History

History
209 lines (155 loc) · 9.1 KB

双重检查锁定漏洞分析笔记.md

File metadata and controls

209 lines (155 loc) · 9.1 KB

双重检查锁定漏洞分析

双重检查锁定模式(也被称为"双重检查加锁优化","锁暗示") 是一种软件设计模式用来减少并发系统中竞争和同步的开销。双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)

该模式在某些语言在某些硬件平台的实现可能是不安全的。有的时候,这一模式被看做是反模式。

它通常用于减少加锁开销,尤其是为多线程环境中的单例模式实现“惰性初始化”。惰性初始化的意思是直到第一次访问时才初始化它的值。

---- from wikipedia

代码的演变:

  1. 最开始的代码:
  • 在多线程环境下运行这份代码将会造成很多错误,最明显的问题是(其他问题在后面):

    假设有线程1,2,当线程1运行到A进入B区将要新建实例时,(此时helper还未赋值)线程2也通过A的判断语句,进入B区新建实例.这样就造成了两个或多个helper被实例化.

      // Single threaded version
      class Foo { 
      private Helper helper = null;
      public Helper getHelper() {
          if (helper == null)             //A
              helper = new Helper();      //B
          return helper;                  //C
          }
      // other functions and members...
      }
    
  1. 加锁代码:
  • 代码1最简单的处理方式是对方法加锁进行同步处理:

      // 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...
      }
    
  1. 双重检查锁定代码:
  • 代码2当中每次获取helper都要进行加解锁操作,开销是很大的.而除了第一次实例化对象,其他时候都只是单纯放回helper对象,不需要同步操作,于是有了代码3:

      // 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...
      }
    
  • 【我是重点】然而问题来了,这份代码在编译器共享内存多处理器的优化(即重排序)下是无效的.

  • 为什么无效?有两个原因,第一个原因(第二个原因看代码4):

    the writes that initialize the Helper object and the write to the helper field can be done or perceived out of order. Thus, a thread which invokes getHelper() could see a non-null reference to a helper object, but see the default values for fields of the helper object, rather than the values set in the constructor.

    大致意思是Helper对象的初始化(这里特指<init>方法,注意区分对象初始化类初始化)和对象写入helper域可以是无序的.因此,一个调用getHelper()的线程可以看到helper对象的非空引用,但是看到的helper的字段是默认值(零值),而不是在构造函数中设置的值。(参考对象创建过程)

    singletons[i].reference = new Singleton();这段代码在Symantec JIT编译后如下:
    显然,对象的分配发生在对象初始化之前,这在Java内存模型中是合法的.

      0206106A   mov         eax,0F97E78h
      0206106F   call        01F6B210                  ; 为单例分配空间
                                                       ; 结果返回到eax
      02061074   mov         dword ptr [ebp],eax       ; EBP 是 &singletons[i].reference 
                                                       ; 将未执行<init>操作的对象存放到此处
      02061077   mov         ecx,dword ptr [eax]       ; 大致是获取原始指针的操作
      02061079   mov         dword ptr [ecx],100h      ; 接下来四行是
      0206107F   mov         dword ptr [ecx+4],200h    ; 单例的<init>操作
      02061086   mov         dword ptr [ecx+8],400h
      0206108D   mov         dword ptr [ecx+0Ch],0F84030h
    
  1. "改进"版代码:
  • 基于上述问题,人们又给出了改进版的代码:
    把对象实例化放入内部的同步代码块中,通过一个内部锁,认为这样可以强制对象实例化完成后才能将对象的引用存入helper域中.

      // (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...
      }
    
  • 然而,这种想法也是错误的!!!

    monitorexit(弹出objectref,释放和objectref相关联的锁的操作码)的规则是: monitorexit操作码之前的操作必须在锁释放之前执行.
    但并没有规定说明monitorexit之后的操作不能在锁释放之前执行.所以编译器移除helper = h是完全合理的.
    许多处理器提供了这种单向内存栅栏的说明,强行改变它的语义为双向内存栅栏将会得到错误的运行结果.

真正的解决方案:

1. 如果你想要的单例是静态的,而不是对象属性:

将单例定义为一个单独的类中的静态字段.Java的语义保证字段不会被初始化,直到字段被引用,并且任何访问字段的线程都将看到所有的初始化该字段的值.

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

class Foo { 
    public Helper getHelper() {
        return HelperSingleton.singleton;
    }
}

//2
class Foo { 

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

    public static Helper getHelper() {
        return HelperSingleton.singleton;
    }
}

2. 单例对象属性:

  1. 使用线程本地存储实现双重检查锁定的巧妙方法.
    每个线程保留一个线程本地标志来确定该线程是否完成了所需的同步。但这取决于LocalThread的存取速度.

     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);
         }
     }
    
  2. 更好的方法: 使用volatile:

系统不允许volatile写操作与之前的任何读写操作重排序,不允许volatile读操作与之后的任何读写操作重排序.
通过声明实例属性是volitile的,保证(对象初始化实例分配之前发生)双重检查锁定.

    // 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;
            }
        }
  • 如果Helper是一个不可变的对象,这样Helper的所有字段都是final的,那么双重检查锁定就可以工作,而不必使用volatile字段 对一个不可变对象(比如一个String或者一个Integer)的引用应该和int或者float类似。读取和写入对不可变对象的引用是原子操作。

说明

笔记内容来自:
The "Double-Checked Locking is Broken" Declaration