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

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

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

Java中的使用

编辑

考虑下面的Java代码[2]页面存档备份,存于互联网档案馆

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

    // other functions and members...
}

这段在使用多线程的情况下无法正常工作。在多个线程同时调用getHelper()时,必须要获取,否则,这些线程可能同时去创建对象,或者某个线程会得到一个未完全初始化的对象。

锁可以通过代价很高的同步来获得,就像下面的例子一样。

// Correct but possibly expensive multithreaded version
class Foo {
    private Helper helper = null;
    public synchronized Helper getHelper() {
        if (helper == null) {
            helper = new Helper();
        }
        return helper;
    }

    // other functions and members...
}

只有getHelper()的第一次调用需要同步创建对象,创建之后getHelper()只是简单的返回成员变量,而这里是无需同步的。 由于同步一个方法会降低100倍或更高的性能[2], 每次调用获取和释放锁的开销似乎是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。许多程序员以下面这种方式进行优化:

  1. 检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。
  2. 获取锁
  3. 第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。
  4. 否则,初始化并返回变量。
// 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...
}

直觉上,这个算法看起来像是该问题的有效解决方案。然而,这一技术还有许多需要避免的细微问题。例如,考虑下面的事件序列:

  1. 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
  2. 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。
  3. 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有覆盖B使用的内存(缓存一致性)),程序很可能会崩溃。


J2SE 1.4或更早的版本中使用双重检查锁有潜在的危险,有时会正常工作:区分正确实现和有小问题的实现是很困难的。取决于编译器,线程的调度和其他并发系统活动,不正确的实现双重检查锁导致的异常结果可能会间歇性出现。重现异常是十分困难的。

J2SE 5.0中,这一问题被修正了。volatile关键字保证多个线程可以正确处理单件实例。[3]页面存档备份,存于互联网档案馆)描述了这一新的语言特性:

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

    // other functions and members...
}

注意局部变量result的使用看起来是不必要的。对于某些版本的Java虚拟机,这会使代码提速25%,而对其他的版本则无关痛痒。[3]

如果helper对象是静态的(每个类只有一个), 可以使用双重检查锁的替代模式惰性初始化模式[4]。查看[5] 上的列表16.6。

// Correct lazy initialization in Java
@ThreadSafe
class Foo {
    private static class HelperHolder {
       public static Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

这是因为内部类直到他们被引用时才会加载。

Java 5中的final语义可以不使用volatile关键字实现安全的创建对象:[6]

public class FinalWrapper<T> {
    public final T value;
    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class Foo {
   private FinalWrapper<Helper> helperWrapper = null;

   public Helper getHelper() {
      FinalWrapper<Helper> wrapper = helperWrapper;

      if (wrapper == null) {
          synchronized(this) {
              if (helperWrapper == null) {
                  helperWrapper = new FinalWrapper<Helper>(new Helper());
              }
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
}

为了正确性,局部变量wrapper是必须的。这一实现的性能不一定比使用volatile的性能更高。

Microsoft Visual C++ 中的使用

编辑

如果指针是由C++关键字volatile定义的,那么双重检查锁可以在Visual C++ 2005 或更高版本中实现。Visual C++ 2005 保证volatile变量是一种内存屏障,阻止编译器和CPU重新安排读入和写出语义。[7] 在先前版本的Visual C++则没有此类保证。在其他方面将指针定义为volatile可能会影响程序的性能。例如,如果指针定义对代码的其他地方可见,强制编译器将指针视为屏障,就会降低程序的性能,这是完全不必要的。

参见

编辑

参考资料

编辑
  1. ^ Schmidt, D et al. Pattern-Oriented Software Architecture Vol 2, 2000 pp353-363
  2. ^ Boehm, Hans-J. "Threads Cannot Be Implemented As a Library", ACM 2005, p265
  3. ^ Joshua Bloch "Effective Java, Second Edition", p. 283
  4. ^ Brian Goetz et al. Java Concurrency in Practice, 2006 pp348
  5. ^ 存档副本. [2012-02-12]. (原始内容存档于2012-03-03). 
  6. ^ [1]页面存档备份,存于互联网档案馆) Javamemorymodel-discussion mailing list
  7. ^ 存档副本. [2012-02-12]. (原始内容存档于2012-10-20). 

外部链接

编辑


📚 Artikel Terkait di Wikipedia

C++ Technical Report 1

reference是由模板类reference_wrapper產生的實體(instance)獲得。wrapper reference近似於C++語言中的引用。 使用ref以獲得任何实例的wrapper reference(對常数引用const &使用cref)。 wrapper reference對模板函数(template

适配器模式

在設計模式中,适配器模式(英語:adapter pattern),有时候也称为包装器(英語:wrapper),它将一个类的接口轉接成客户所预期的接口。适配器使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将要被适配的已存在的类实例或接口,包裹在预期的目标接口的实现之中。

门面模式

门面模式(英語:facade pattern),也翻译为外观模式,是軟件工程中常用的一種軟件設計模式,它為子系統中的一組介面提供一個統一的高層介面,使得子系統更容易使用。 门面模式是面向对象程序设计中常用的一个设计模式。门面的概念类似于一个建筑学中的立面,门面作为一个前端接口来屏蔽更复杂的底层或结构代码。门面模式可以用来:

观察者模式

觀察者模式(英語:observer pattern),是一種軟體設計模式。在此種模式中,一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用在即時事件處理系統。 在上面的UML类图中,Subject类不直接更新依

单例模式

在面向对象编程中,单例模式(英語:singleton pattern),是一种常用的创建型软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这

享元模式

在计算机编程中,享元模式(英語:flyweight pattern),是一種軟體設計模式。它使用物件用來儘可能減少記憶體使用量;於相似物件中分享儘可能多的資訊。當大量物件近乎重複方式存在,因而使用大量記憶體時,此法適用。通常物件中的部分状态(英语:State (computer

惰性初始化

invoke_from_class(): def __init__(self, fn): self.fn = fn functools.update_wrapper(self, fn) def __get__(self, obj, objtype=None): self.from_class = True

Python

定义修饰器的示例: def viking_chorus(func): def wrapper(*args, **kwargs): for i in range(3): func(*args, **kwargs) return wrapper 调用修饰器的示例: @viking_chorus def menu_item(*args):