文档章节

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

Float_Luuu
 Float_Luuu
发布于 2016/05/03 01:00
字数 2159
阅读 2.5K
收藏 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
粉丝 229
博文 47
码字总数 104674
作品 0
长宁
高级程序员
私信 提问
加载中
此博客有 4 条评论,请先登录后再查看。
【opencv】图形的绘制

1.矩形图像的绘制: 原函数:void cvRectangle(CvArr* img, CvPoint pt1, CvPoint pt2, CvScalar color, int thickness=1, int line_type=8,int shift=0) img就是需要绘制的图像 pt1 and pt......

其实我是兔子
2014/10/08
1.2K
1
libqt4json

libqt4jon 是一个使用 Qt QVariant 对象的 JSON 序列化和反序列化库。可系列化原生类型如 integer, double, QString, lists, maps, and QObject recursively. 只序列化通过 QObject Q_PROPER...

匿名
2013/03/29
386
0
Java™ 编译器--Janino

Janino是一个超级小但又超级快的Java™ 编译器. 它不仅能像javac工具那样讲一组源文件编译成字节码文件,还可以对一些Java表达式,代码块,类中的文本(class body)或者内存中源文件进行编译,...

匿名
2013/04/02
4.2K
0
jquery对象/标签映射扩展--NickName

jquery对象/标签映射扩展-NickName OTM是什么 以往把这样的一个json对象 var data = {}; data.UserId = "8888"; data.UserName = "赵六"; data.School="湖北工业大学"; data.schoolNo=100002......

知鸣
2013/06/13
1K
0
理解Swift中Optional类型-有和无的哲学

原文连接:http://blog.barat.cc/ios/understanding-swift-optional/ nil的遗憾 当某个变量或表达式没有任何内容时,在Objective-C中可以使用来表示。在Objective-C中是一个「野孩子」,voi...

巴拉迪维
2015/11/19
1.8K
9

没有更多内容

加载失败,请刷新页面

加载更多

Model S被18轮重卡撞烂 乘客在车辆保护下幸存

日前,国外一位名为quarm813的网友在社交媒体分享了“Model S救他和女儿性命”的经历。 据该用户描述,当地时间7月31日,他驾驶Model S在高速公路快车道上行驶时,一辆18轮重卡突然实线并线闯...

osc_fipgtxy8
20分钟前
4
0
Redis-cluster5.x集群搭建

1.下载redis5.0.2 wget http://download.redis.io/releases/redis-5.0.2.tar.gz #官网下载 tar xzf redis-5.0.2.tar.gz #解压cd redis-5.0.2 yum install gcc #需要gcc来编......

osc_zzg7fpke
22分钟前
11
0
CGB2004-京淘项目Day12

1.还原系统配置 1.1 释放Linux资源 1.1.1 停止数据库主从服务 1.1.2 关闭数据库服务 说明:关闭数据库服务器. 1.1.3 关闭tomcat/mycat服务器 1.1.4关闭nginx服务器 1.2 修改代码中的配置 1.2....

osc_3361hjxk
23分钟前
8
0
【北京迅为】初识i.MX6ULL终结者开发板

目录 一、 开发板初体验 1. 初识i.MX6ULL终结者开发板 一、 开发板初体验 i.MX6ULL终结者开发板是北京迅为电子推出的一款Cortex-A7架构的开发板。采用核心板+底板的方式,如下图所示: 经典蓝...

osc_0esgtdby
23分钟前
8
0
如何利用基于PXI的下一代ATE系统测试平台进行军事/航天/卫星电子设备测试

前言 自动测试设备(ATE)系统用于在生产产品或产品使用过程中测试电子组件,子组件或完整系统的功能和性能,以确保他们可操作性。对设备、电路板、子组件或系统的测试要求从简单到复杂,设计...

osc_mxz6aybo
25分钟前
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部