【分析】声明“双重检查锁坏了”

10 篇文章 0 订阅
1 篇文章 0 订阅

双重检查锁在多线程环境中作为一种有效的实现延迟初始化的方法被广泛在使用。
不幸的是,在java实现的平台中,它将不会可靠的工作而没有额外的同步 。当在其他语言中实现时,例如 c++,双重检查锁依赖处理器的内存模型。在编译器与同步函数库之间,编译器是重新排序执行的。所以少数情况下,c++语言能够工作。即基于少数c++的编译器的内存模型使用内存屏障可以使双重检查锁正常的工作,但是在java中却是不起作用的。
-例如下面的代码:

// 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对象都会被分配,所以很多结果可能都会错误。

// 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…
}

上面的代码中,每次执行getHelper()方法都会执行同步。为了避免多余的同步,对已经分配helper对象进行处理。

// 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…
}

不幸的是,在优化编译器或共享内存处理器中,上面的代码都是不工作的。
不工作的根本原因:不能使访问helper对象的线程执行同步。
它不工作的第一个原因是:初始化Helper对象与Helper对象内容写入完成两个阶段次序颠倒造成的。因此,一个线程每次调用getHelper()方法能够看到一个非null的helper对象(已经初始化完成),但是看到的是默认的对象值,而不会是构造函数中设置的值(对象内容没有写入完成)。
即使编译器不对这些写入进行重排,在一个多线程处理器或者是内存系统中重排这些写入。
基于编译器重排http://gee.cs.oswego.edu/dl/cpj/jmm.html
它不能工作的一个测试用例:
//TODO
当运行在Symantec JIT中,特别说明,在Symantec JIT编译
singletons[i].reference = new Singleton();
结果如下(Symantec JIT使用的是handle-based对象分配系统)

0206106A mov eax,0F97E78h
0206106F call 01F6B210 ; allocate space for
; Singleton, return result in eax
02061074 mov dword ptr [ebp],eax ; EBP is &singletons[i].reference
; store the unconstructed object here.
02061077 mov ecx,dword ptr [eax] ; dereference the handle to
; get the raw pointer
02061079 mov dword ptr [ecx],100h ; Next 4 lines are
0206107F mov dword ptr [ecx+4],200h ; Singleton’s inlined constructor
02061086 mov dword ptr [ecx+8],400h
0206108D mov dword ptr [ecx+0Ch],0F84030h

正如你看到的。先执行singletons[i].reference,后执行构造方法。这在java内存模型中完全合法,并且c/c++同样(他们都没有一个内存模型)。
修复它不工作。从上面的解释中,一些人建议以下修复代码。

// (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…
}

上述代码,将分配helper对象与初始化helper对象分别都进行了同步,使用内存屏障来避免重排。
不幸的是,直觉告诉我们那种方案绝对是错的。这种同步的规则是不能工作的。因为monitorexit(释放同步锁)必须在monitor释放之前执行。编译器分配helper = h的时候,这种情况和先前的例子一样。大部分处理器提供的指令都是这种单向的内存屏障,语义变化需要性能损失的去释放锁去做一个内存屏障。
这个时候,你可能会想到强制写入双向内存屏障,但是一旦java内存模型修改,这种方案就不能工作了。如果你感兴趣,可以看方案地址,但尽量不要使用。
方案地址:http://www.cs.umd.edu/~pugh/java/memoryModel/BidirectionalMemoryBarrier.html
然而,即使与一个完整的内存障碍被执行的线程去初始化helper对象,它仍然不工作。

为什么?因为处理器都有一个他们自己本地的缓存副本,在一些处理器中,除非处理器执行一个缓存一致性指令,读取执行一直被更新的缓存副本,即使有些处理器使用内存屏障强制写入全局内存。
这个链接地址讨论了这种情况的发生。http://www.cs.umd.edu/~pugh/java/memoryModel/AlphaReordering.html

问题是否存在价值?
对于大多数应用程序,简单的处理getHelper()方法被同步的开销是不太大的。你应该考虑的是这种详细的优化增加了程序的成本开销。
通常,更多高水平的聪明的,比如使用归并排序而不是交换排序(SPECJVM DB 基准)将要有更大的影响。
使得helper对象静态单例化:
创建了一个static的Singleton,仅有一个Helper对象。其他的对象Foo对象引用同一个Helper对象。
在一个指定的类中定义一个static的单例对象,java语义保证这个字段不会被初始化,直到这个字段的引用改变。并且任何一个访问这个字段的线程都将会看到这个字段的初始化结果。
class HelperSingleton {
static Helper singleton = new Helper();
}
工作在32-bit原语方案

// Correct Double-Checked Locking for 32-bit primitives
class Foo {
private int cachedHashCode = 0;
public int hashCode() {
int h = cachedHashCode;
if (h == 0)
synchronized(this) {
if (cachedHashCode != 0) return cachedHashCode;
h = computeHashCode();
cachedHashCode = h;
}
return h;
}
// other functions and members…
}

实际上,computeHashCode()方法是幂等的话,就可以不用同步。

// 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…
}

使用内存屏障使工作
如果你有明确的内存屏障指令的话,可以使用双重检查锁去工作。例如,你用c++语言。以下代码

// C++ implementation with explicit memory barriers
// Should work on any platform, including DEC Alphas
// From "Patterns for Concurrent and Distributed Objects",
// by Doug Schmidt
template <class TYPE, class LOCK> TYPE *
Singleton<TYPE, LOCK>::instance (void) {
// First check
TYPE* tmp = instance_;
// Insert the CPU-specific memory barrier instruction
// to synchronize the cache lines on multi-processor.
asm ("memoryBarrier");
if (tmp == 0) {
// Ensure serialization (guard
// constructor acquires lock_).
Guard<LOCK> guard (lock_);
// Double check.
tmp = instance_;
if (tmp == 0) {
tmp = new TYPE;
// Insert the CPU-specific memory barrier instruction
// to synchronize the cache lines on multi-processor.
asm ("memoryBarrier");
instance_ = tmp;
}
return tmp;
}


使用本地线程存储解决双重检查锁
每一个线程保持线程本地的标记以确保该线程所需要做的同步。

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的性能。1.2很慢,大概比较快的在1.4
性能良好的实现延迟加载方案地址:http://www.cs.umd.edu/~pugh/java/memoryModel/DCL-performance.html
在新的java内存模型上
jdk1.5,java内存模型规范。http://www.cs.umd.edu/~pugh/java/memoryModel/
解决双重检查锁可以使用Volatile

// 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的,那么双重检查锁其实是没有必要的。不可变的话引用对象(String,Integer)表现几乎和int,float一致。向不可变对象读和写引用都要保持原子性。

原文地址:
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值