`
ykdsg
  • 浏览: 17153 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

双重检查锁定的漏洞的分析 The "Double-Checked Locking is Broken" Declaration

 
阅读更多

本文根据http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html来翻译,纯粹为了自己学习做记录,有生硬不通的地方还请海涵,也欢迎各位朋友指正。

在多线程环境下实现延迟加载时 Double-Checked Locking是通常使用的而且效率比较高的方法。不幸的是,如果没有其他同步机制的话,他也许不能在java平台可靠的运行。当使用其他语言实现时,比如c++,这取决于处理器的内存模型,编译器引起的reordering 和编译器与synchronization 库之间的相互作用。因为这些不是针对特定的语言,比如c++,几乎可以说会在其中一种情况工作。显示的内存屏障(memory barriers)可以在c++中使用,但是不能用在java中。

首先解释期望的动作,思考下面的代码:

如果这个代码用于多线程环境,很多事都会出错。最明显的,两个或者更多Helper对象会被创建(我们后续会讲述其他问题)。简单解决这个问题是对getHelper()方法使用synchronize 关键字。

上面这段代码每次访问getHelper()方法都要同步进行,双重检查锁定(double-checked locking )试图避免在helpser对象创建后的同步化。

不幸的是,这个段代码将不能按期望工作在优化编译器或共享内存的多处理器。

他不能工作

不能工作有很多原因,下面描述的第一个原因很明显的。理解了这些,你也许会被诱惑着去设计修正双重检查锁定的方法。但是你的解决方案不能工作:有很多微妙的原因。理解这些原因之后想出更好的解决方案,但是这个也不能工作,因为还有更多微妙的原因。

很多聪明的人花了很多时间关注这个,但是除了让每个线程同步访问helpser对象外没有办法解决这个问题。

不能工作的第一个原因

不能工作的最明显原因是初始化Helper对象和helpser字段的赋值可能按顺序进行也可能颠倒。因此一个线程调用getHelper()能得到非空的helper对象引用,但是看到是helper里的字段是初始值,而不是构造函数里设置的值。

如果编译器内联到构造函数的调用且能保证构造函数不会抛出异常或执行同步。,那么初始化对象的操作和对helper 里字段的写入可以自由重排。

即使编译器没有重排这些操作,在一个多线程处理器或内存系统可能会重排这些写操作,当感知到另一个线程在另一个处理器上执行时。

Doug Lea写了一篇more detailed description of compiler-based reorderings.

测试用例,说明他不能工作

Paul Jakubik 发现一个使用双重检查锁定但是不能正确工作的例子.A slightly cleaned up version of that code is available here.

在使用Symantec JIT编译器的系统上不能工作。特别是Symantec JIT 编译

如下所示(注意Symantec JIT 使用基于句柄的对象分配系统)

就像你看到的,分配给singletons[i]的应用是在Singleton的构造函数被执行前。这在java的内存模型中是完全合法的,而且在c和c++中也同样。

一个不能工作的修正方案

根据上述的解释,一些人可能会给出下面的代码:

这段代码将Helper对象的构造函数放入synchronized 块内,直观的想法是在同步块释放的点有内存屏障(memory barrier ),这样能够避免颠倒初始化Helper和对helper字段赋值的顺序。

不幸的是,这个是完全错误的。这个同步规则将不会这么工作。对于监视器退出规则(比如:释放同步)是指在监视退出前的操作必须在监视器释放前执行。然而没有规则说那些在监视器退出之后的操作不能在监视器释放前完成。编译器将变量的分配helper = h放入同步块是完全合法的,在这种情况下我们又回到了之前的地方。许多处理器提供执行这种单向内存屏障的指令。但是变更语义要求释放锁是一个完整的内存屏障将有性能损失。

更多的不能工作的修补程序

有些操作可以强制写进程执行完全的双向内存屏障。但这是粗劣,低效的,而且一旦java内存模型改变基本上就不能工作。不要使用这些技术。

但是即使在helper对像初始化时,一个完整的内存屏障被线程执行,他仍然无法正常工作。

问题是在某些系统中,那些看到helper字段非空值的线程也需要执行内存屏障。

为什么?因为处理器有他们自己本地的内存副本。某些处理器,如果没有执行缓存一致性指令(例如,内存屏障),读线程可能读到过期的本地缓存副本,即使其他处理器使用内存屏障强制写进程写到主内存。这里专门讨论这种情况怎样发生在Alpha 处理器上 a separate web page

是否值得这么麻烦?

对大多数应用来说,简单的将getHelper()方法同步化的成本并不高。只有在你知道这会造成应用程序重大开销而且可以接受的情况下才考虑这种优化方案。

很多时候,更聪明的方法是使用内建的归并排序而不是处理交换排序(见 JVM DB基准说明)会更有效。

在静态单例的情况下工作

如果你建立的单例是静态的(比如,只有一个Helper对象会被创建),相对于另一种对象属性(比如,每一个Foo对象有一个Helper对象)有一种简单优雅的解决方法。

只要在一个单独的类中定义一个静态字段。Java的语义保证字段不会被初始化直到字段被引用,而且所有使用该字段的线程将看到所有在初始化字段时的写入结果。

在32位原始值上工作

虽然双重检查锁定不能用于引用类型对象,但是他可以用于32位原始类型(比如,int或float)。注意他不能工作于long或者double,因为非同步的64位原始类型的读写不保证是原子的。

事实上假设方法computeHashCode()总是返回相同的结果而且没有副作用(比如,幂等--一个操作不会修改状态信息,并且每次操作的时候都返回同样的结果。即:做多次和做一次的效果是一样 的。)你甚至可以去掉所有的同步。

使用显示的内存屏障

如果你有明确的内存屏障指令他可能会让双重检查锁定模式工作。比如,如果你使用c++,你可以使用Doug Schmidt 等人书中的代码:

使用ThreadLocal 存储

Alexander Terekhov 提出了一个聪明的建议:使用thread local存储来实现双重检查锁定。每个线程保留一个线程本地标志来确定该线程是否已完成所需的同步。

这种技术的性能相当程度上取决于你使用的JDK,在JDK1.2上很慢,但是在JDK1.3之后的版本就快很多。

新的java内存模型

JDK5,a new Java Memory Model and Thread specification.

是用Volatile关键字

JDK5及以后的版本扩展了volatile的语义,这样系统将不允许对一个volatile的写操作与之前的读写操作进行重排。而且对volatile的读操作也不会与后续的读写进行重排。详细请见: this entry in Jeremy Manson's blog

根据这个双重检查锁定可以在helper字段声明为volatile时正常工作,但是在JDK4及之前不行。

双重检查锁定不变对象

如果Helper是不可变对象,比如Helper所有字段都是final,那么不是用volatile关键字双重检查锁定也能正常工作。这个思路是一个不可变对象(比如,String 或 Integer)在很大程度上就像int或float;读写不可变对象是原子的。

分享到:
评论

相关推荐

    C++ and the Perils of Double-Checked Locking

    在介绍双检锁模式(Double-Checked Locking Pattern,DCLP)的C++实现中,Scott Meyers和Andrei Alexandrescu在其2004年的文章中指出,传统的单例模式实现并不具备线程安全性。单例模式是设计模式中经常被提及的一种...

    C++ and the Perils of Double Checked Locking.zip

    《C++ and the Perils of Double Checked Locking》是一篇探讨C++编程中双重检查锁定(Double-Checked Locking)模式潜在问题的文献。在多线程编程中,双重检查锁定是一种常见的优化策略,旨在减少对同步原语的依赖...

    c++ and Peris of Double Checked Locking

    标题:C++与双检查锁定(Double Checked Locking)的陷阱 描述:C++如何解决单例模式的线程安全问题 ### 关键知识点解析: #### 单例模式的线程安全挑战 单例模式是一种设计模式,确保一个类只有一个实例,并提供...

    AngularJS入门教程之ng-checked 指令详解

    本文将针对AngularJS中的ng-checked指令进行详细解析。 ng-checked指令是用于在AngularJS中动态控制复选框(checkbox)或单选按钮(radio)的选中状态。当你需要根据数据模型(model)中某个变量的真值(true)或假值(false...

    深入剖析Java中的双检锁模式:实现、陷阱与最佳实践

    在Java并发编程中,双检锁(Double-Checked Locking)是一种用于减少同步开销的优化技术,尤其适用于懒加载(lazy initialization)的场景。本文将详细探讨双检锁的工作原理、潜在问题以及如何安全地实现它。 双检锁...

    element-ui tree树形控件:default-checked-keys 设置空数组 默认还是选中状态问题

    在这个问题中,开发者遇到了一个关于`default-checked-keys`属性的问题,即当尝试将该属性设置为空数组时,树形控件的节点依然保持选中的状态,这与预期的行为不符。 `default-checked-keys`属性是Element UI Tree...

    Java-设计模式-单例模式-实现源码(简单实现、双重检查锁、静态内部类、枚举类)

    在Java中,有多种实现单例模式的方法,包括简单实现、双重检查锁定(Double-Checked Locking)、静态内部类和枚举类。下面我们将详细探讨这些不同的实现方式。 1. **简单实现(非线程安全)** 最简单的单例实现...

    Java双重检查加锁单例模式的详解

    DCL(Double-checked locking)是Java双重检查加锁单例模式的一种实现方法。它使用了synchronized关键字来确保线程安全,但是这也会带来性能损失。DCL看起来是一个聪明的优化,但是它却不能保证正常工作。 在多线程...

    Java多线程之延迟初始化1

    为了解决性能问题,引入了"双重检查锁定"(Double-Checked Locking)策略: ```java public class DoubleCheckedLocking { private volatile static Instance instance; public static Instance getInstance() { ...

    深究AngularJS——ng-checked(回写:带真实案例代码)

    本文深入探讨了AngularJS框架中的ng-checked指令,通过真实案例代码的形式展示了如何使用ng-checked实现复选框的状态记忆与回写。ng-checked指令在AngularJS中用于获取和设置复选框(checkbox)的选中状态。它依赖于...

    双重检查锁

    双重检查锁(Double-Checked Locking, DCL)是一种在多线程环境中用于实现懒加载(lazy loading)的设计模式。它通过两次检查来确定是否需要获取锁,从而避免不必要的同步操作,提高程序性能。然而,DCL的实现并不像表面...

    Java模式设计之单例模式[借鉴].pdf

    **双重检查锁定(DCL,Double-Checked Locking)**: 为了解决懒汉式的性能问题,有人提出了双重检查锁定,试图在保证线程安全的同时减少同步开销。然而在Java中,早期的DCL实现并不完全正确,因为Java内存模型的...

    Software Adaptation in an Open Environment: A Software Architecture Perspective

    Software Adaptation in an Open Environment: A Software Architecture Perspective by Yu Zhou ...The organization and presentation of the book will be double-checked by professional scholars

    ACE中的DoubleCheckedLocking模式

    北京火龙果软件工程技术中心意图无论什么时候当临界区中的代码仅仅需要加锁一次,同时当其获取锁的时候必须是线程安全的,可以用DoubleCheckedLocking模式来减少竞争和加锁载荷。动机1、标准的单例。开发正确的有效...

    Java多线程编程环境中单例模式的实现

    为了提高性能,人们提出了**双重检查锁定**(Double-checked locking)的方法。这种方法首先在不加锁的情况下检查`instance`是否为`null`,如果为`null`则进行同步操作: ```java public static Singleton ...

    经典设计模式

    3. **双重检查锁定**(Double-Checked Locking):这是一种优化的线程安全实现,它试图在保持线程安全性的同时减少同步开销。在这个实现中,先检查`instance`是否为`null`,然后才进行锁定。只有当`instance`确实为`...

    checked.css-超酷CSS3复选框和单选按钮点击动画库

    这里,`checked--effect-name`需要替换为23种动画效果中的一个,如`checked--pulse`或`checked--rotate`。 3. **自定义样式**:除了预设的动画效果,你还可以通过CSS对复选框和单选按钮的样式进行个性化调整,以...

    自定义redis序列化和解决穿透问题.docx

    在业务逻辑中添加锁机制,如双重检查锁定(DCL,Double-Checked Locking),确保在多线程环境下对同一数据的处理是串行的,避免数据同步延迟。 双重检查锁定示例: ```java public class Singleton { private ...

    设计模式java实现之单例模式1

    4. **线程安全旗舰版 - 双重检查锁定(DCL,Double-Checked Locking)**: DCL模式结合了延迟加载和线程安全,它在初始化实例时使用双重检查,避免了无谓的同步开销。`volatile`关键字确保了`uniqueInstance`的可见...

Global site tag (gtag.js) - Google Analytics