“双检测锁”机制

引述[0]

双检测机制主要用于多线程环境下的延迟初始化,也经常和单件(Singleton)模式在一起使用。如果只讨论Singleton模式,不必这么麻烦。

 

下面是一段双检测锁代码:

 

// 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)的初始化顺序是不确定的。比如,线程调用方法getHelper(),可以获取一个非空的helper的引用,但是可能发现helper对象成员的值不是正确的应该在构造函数执行之后的值。如果创建对象和初始化对象是一步执行(inline)的,那两者的顺序就无所谓。

 

在Symantec JIT的虚拟机上的测试验证了双检测的问题。比如下面的语句:

singletons[i].reference = new Singleton();

赋值操作是在构造函数之前执行的。

 

在Singleton模式下,如果可以只有一个对象,可以定义静态成员。文中给的例子是在一个单独的类中定义:

class HelperSingleton {
  static Helper singleton = new Helper();
}
 还可以使用线程本地存储(Thread Local Storage)解决双检测机制
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);
         }
}

 上面的代码显示可以保证每个线程的getHelper()是安全的和正确的。在Java5及以后的版本里,采用了新的内存模型,可
以使用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;
        }
    }
 
给变量添加volatile关键字,指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。而不是本地内存(比如机器的寄存器)。双检测机制联合使用volatile后,系统不允许对变量的写操作和之前的读写操作进行优化重排执行(reorder),同时,不会对读操作和之前的读写操作进行优化重排执行。
( the system will not allow a write of a volatile to be reordered with respect to any previous read or write, and a read of a volatile cannot be reordered with respect to any following read or write.)
结合代码,如果helper已经new出来,但是还没有运行构造函数,此时另外一个线程的访问会延后到构造函数执行完毕才成功。就是说如果已经开始对helper进行写操作(new及构造初始化),则读操作(获取引用)不会成功。也就是说,volatile 变量可以被看作是一种 “程度较轻的 synchronized。

一个极端的例子,如果对象是一个不可变对象(immutable object),所有的成员都是final的,双检测机制可以工作的很好,因为访问一个不可变对象和访问原生类型,比如int,float等32位对象是没有什么区别的,都是原子操作。


总结实现考虑
1. 如果只是在Singleton模式下,可以简单使用静态变量,不适用延迟加载,不会有任何问题
2. 如果一定想延迟加载,加上volalite关键字
3. 如果不记得volalite关键字,使用同步方式,不使用双检测。


别的语言
1. C++: 可以结合内存栅栏(memory barriers),思路类似Java中volalite的效果。代码好似从ACE中摘的,但是我看最新的ACE代码,Singleton实现没有发现这样使用,这个问题暂且存疑。
2. Erlang:上面说了这么多,这个问题在Erlang中完全不存在。Erlang的变量不会改变,程序逻辑的流转依靠消息。这也算是Erlang的一个优势吧。


参考:
0. The "Double-Checked Locking is Broken" Declaration http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
1. 正确使用volatile: http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值