文档章节

【译】可以不要再使用Double-Checked Locking了

Float_Luuu
 Float_Luuu
发布于 2016/05/03 01:00
字数 2159
阅读 1066
收藏 5

Double-Checked Locking方法被广泛的使用于实现多线程环境下单例模式的懒加载方式实现,不幸的是,在JAVA中,这种方式有可能不能够正常工作。在其他语言环境中,如C++,依赖于处理器的内存模型、编译器的重排序以及编译器和同步库之间的工作方式。由于这些问题在C++中并不确定,因此我们不能够确定具体的行为。但是在C++中显示的内存屏障是可以被用来让其正常工作的,而这些屏障在JAVA中又不好用。


一、Double-Checked Locking入门

首先来看看下面这段代码我们期望得到的行为:

// 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对象将会被分配内存,其他问题我们会在后面提到,我们先简单的给方法加一个synchronized关键字。

// 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方法的时候都要进行同步,下面的Double-Checked Locking方式避免了当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...
  }

不幸的是,这段代码在存在编译优化或多处理器共享内存的情况下不能够正常工作。


二、Double-Checked Locking不能够正常工作

为什么上文说Double-Checked Locking不能够正常工作有很多的原因,我们将会描述一对很显而易见的原因。通过理解存在的问题,我们尝试着去修复Double-Checked Locking存在的问题,然而我们的修复可能并没有用,我们可以一起看看为什么没有用,理解这些原因,我们去尝试着寻找更好的方法,可能还是没有用,因为还是存在一些微妙的原因。


1)第一个不能正常工作的原因

Double-Checked Locking不能够正常工作的一个很显然的原因是对helper属性的写指令和初始化Helper对象的指令可能被冲排序,因此当其他线程再次调用getHelper方法的时候,将会得到一个没有被初始化完成的Helper对象,如果这个线程访问了这个对象没有被初始化的属性,那么就会出现位置错误。

我们来看看对于下面这行代码,在Symantec JIT编译器环境下的指令重排序的例子:

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

下面是实际执行的代码:

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的赋值操作是在构造Singleton对象之前,这在当前的JAVA内存模型中是完全合法的,在C和C++中也是合法的。


2)一种无用的修复

理解了上面的问题,有些同学给出了下面的这段代码,试图避免问题:

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

上面的代码将对象构造放在一个内部的synchronized块里面,直觉的想法是想通过synchronized释放之后的屏障来避免问题,从而阻止对helper属性的赋值和对Helper对象的构造的指令重排序。不幸的是,直觉是错误的。因为synchronization的规则能保证所有在monitorexit之前的动作都能够生效而并不包含在monitorexit之后的动作在monitorexit之前不生效。也就是我们能够保证在退出内部同步块之前Helper能够被实例化,h能够被复制,但是不能保证helper被赋值一定发生在退出同步块之后,因此同样会出现没有被构造完的Helper实例被其他线程引用并访问。


3)其他无用的修复

我们可以通过完全双向的内存屏障来强制行为生效,这么做是粗鲁的,非高效的,并且几乎可以保证一旦JAVA内存模型被修订,原有方式将不能够正常工作。所以,请不要这么做。然而,即使通过完全内存屏障,还是不能够正常工作。问题是在一些系统上,线程对非空的helper属性字段同样需要内存屏障。为什么呢?因为处理器拥有自己的缓存,在一些处理器中,除非处理器执行缓存一致性指令,否则将有可能从缓存读取错误内容,尽管其他处理器将内容从缓存刷新到了主存。


4)至于搞这么复杂么?

在很多应用中,简单的将getHelper方法同步开销其实并不大,除非能够证明其他优化方案确实能够为应用带来不少的性能提升。


三、使用静态域

如果我们正要创建的实例是static的,我们有一种很简单的方法,仅仅将单例静态属性字段在一个单独的类中定义:

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

这么做既保证的懒加载,又保证单例被引用的时候已经被构造完成。


四、Double-Checked Locking对32位原始类型有效

尽管Double-Checked Locking对对象引用类型无效,对于32位原始类型却是有效的,值得注意的是对64位的long和double类型并不是有效的,因为64为的long和double不能够保证被原子地读写。

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


五、利用ThreadLocal修复Double-Checked Locking问题

Alexander Terekhov提出了一个聪明的方法,通过ThreadLocal来实现Double-Checked Locking,每个Thread保持一个local flag来标识当前线程是否已经进入过同步块:

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版本,在Sun公司的JDK1.2版本中,ThreadLocal是很慢的,在1.3版本之后变得非常快了。


六、在新的JAVA内存模型中

在JDK1.5或者更晚的版本中,扩展了volatile的语义,使得我们可以通过将helper属性字段设置为volatile来修复Double-Checked的问题:

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

   

七、使用不变实例

还有一种方法是讲单例对象变为不可变对象,如所有字段都声明为final或者类似String类或Integer类这种。


本文由博主翻译改编自:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 欢迎转载,如有内容错误,还请多多包涵。

© 著作权归作者所有

Float_Luuu
粉丝 219
博文 47
码字总数 104674
作品 0
长宁
高级程序员
私信 提问
加载中

评论(4)

Float_Luuu
Float_Luuu 博主

引用来自“幻影狼人”的评论

可以不要再使用Double-Checked Locking了,事实上你仍然要用,而且无法避免
个人觉得内部类、静态域这种方式蛮好的
Float_Luuu
Float_Luuu 博主

引用来自“幻影狼人”的评论

本以为是新内容,点进来一看又是老生常谈的问题,有严重的标题党嫌疑。

1.在Java9欲出的当前时间点,JDK版本至少不应该低于6.0了,6.0是一个里程碑版本,其升级特性有着明显的提升,再去研究那些已经过时了的环境机制没有任何生产意义。

2.单例模式在Java中已经被说烂了,最佳实践就是三种:
直接声明、volatile双校验、枚举单例
这个东西就跟月经一样,过一段时间就又有人会拿出来消费一遍,然而说的都是重复的东西

3.虽然直接声明和枚举单例是推荐优先使用的单例模式,但是在某些情况下,双校验有他实际的使用场景,另外两种无法替代,例如,在Android中,有上下文系统,这种写法非常常见:


public final class SomeUtils {

private volatile static SomeUtils singleton;

public static SomeUtils with(Context context) {
if (singleton == null) {
synchronized (SomeUtils.class) {
if (singleton == null) {
singleton = new SomeUtils(context);
}
}
}
return singleton;
}

private final Context context;

private SomeUtils(Context context) {
this.context = context.getApplicationContext();
}

// 一些方法

}






这个问题已经被翻来覆去的说过多少变了,
一方面温故而知新吧,另一方面你知道很多遍别人也不一定知道哈
幻影狼人
幻影狼人
可以不要再使用Double-Checked Locking了,事实上你仍然要用,而且无法避免
幻影狼人
幻影狼人
本以为是新内容,点进来一看又是老生常谈的问题,有严重的标题党嫌疑。

1.在Java9欲出的当前时间点,JDK版本至少不应该低于6.0了,6.0是一个里程碑版本,其升级特性有着明显的提升,再去研究那些已经过时了的环境机制没有任何生产意义。

2.单例模式在Java中已经被说烂了,最佳实践就是三种:
直接声明、volatile双校验、枚举单例
这个东西就跟月经一样,过一段时间就又有人会拿出来消费一遍,然而说的都是重复的东西

3.虽然直接声明和枚举单例是推荐优先使用的单例模式,但是在某些情况下,双校验有他实际的使用场景,另外两种无法替代,例如,在Android中,有上下文系统,这种写法非常常见:


public final class SomeUtils {

private volatile static SomeUtils singleton;

public static SomeUtils with(Context context) {
if (singleton == null) {
synchronized (SomeUtils.class) {
if (singleton == null) {
singleton = new SomeUtils(context);
}
}
}
return singleton;
}

private final Context context;

private SomeUtils(Context context) {
this.context = context.getApplicationContext();
}

// 一些方法

}






这个问题已经被翻来覆去的说过多少变了,
【单例设计模式】单例模式中为什么用枚举更好

枚举单例(Enum Singleton)是实现单例模式的一种新方式,尽管单例模式在java中已经存在很长时间了,但是枚举单例相对来说是一种比较新的概念,枚举这个特性是在Java5才出现的,这篇文章主要...

冷冷gg
2016/08/30
142
0
volatile应用场景之--Double check

推荐两篇文章 https://blog.csdn.net/dl88250/article/details/5439024 http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization 讲的非常好,解释了为啥需要在......

karma123
2018/08/31
14
0
Singleton 的较好的实现方法

静态持有者单例模式(static holder singleton pattern) 有三个好处: 第一,静态工厂;第二,延迟初始化;第三,线程安全。 2. 双重检查锁(singleton with double checked locking) 参考...

圣洁之子
04/02
8
0
模板引擎 Velocity 1.6.4 发布-下载

在 Velocity 1.7 正式版发布之前发布的 1.6.4 版本主要是为了修复三个高危的bug。 该版本修正了三个问题: 1. 修正了 #parse 中当IncludeEventHandler 返回null时导致的空指针异常 2. Fix d...

红薯
2010/05/19
1K
0
透过 Linux 内核看无锁编程

非阻塞型同步 (Non-blocking Synchronization) 简介 如何正确有效的保护共享数据是编写并行程序必须面临的一个难题,通常的手段就是同步。同步可分为阻塞型同步(Blocking Synchronization)...

文艺小青年
2017/06/09
0
0

没有更多内容

加载失败,请刷新页面

加载更多

米联客(MSXBO)USB3.0 UVC摄像头实现基于FT602Q芯片方案

USB3.0 UVC摄像头实现基于FT602Q芯片方案 USB3.0接口芯片FT602Q支持UVC协议,可以很方便的实现一个USB相机。这里我们采集HDMI输入的视频信号,实现了一个USB3.0的HDMI采集卡。 逻辑结构如下图...

msxbo
36分钟前
4
0
未初始化指针问题

《C和指针》书上说 int *a ... *a = 12 这样写声明一个变量,但未对指针初始化 如果指针是函数的形参,比如 void func(int *a) { (* a) = 12;//这样操作有无问题? } ======================...

天王盖地虎626
52分钟前
7
0
Python的一些细节 II

1. isinstance() 与 type() 区别 class type(name, bases, dict) name -- 类的名称。 bases -- 基类的元组。 dict -- 字典,类内定义的命名空间变量。 返回值:一个参数,返回对象的类型;三...

Eappo_Geng
今天
4
0
笔试题-武汉珞珈德毅笔试题

1.写出Java语言的基本数据类型。 2.简述cookie和session区别。 1、cookie数据存放在客户的浏览器上,session数据放在服务器上。 2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行...

QuasimodoALei
今天
7
0
IDEA Maven project: 'xxx/pom.xml' already exists in VFS

Failed to create a Maven project: ‘xxx/pom.xml‘ already exists in VFS idea创建项目后,发现项目有问题,删除后重新创建,提示错误如下。 解决办法 1.通过idea打开任意一个项目 2.File...

国产大熊猫
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部