`

用happen-before规则重新审视DCL

阅读更多

转载自 ---- http://lifethinker.iteye.com/blog/260515

 

      编写Java多线程程序一直以来都是一件十分困难的事,多线程程序的bug很难测试,DCL(Double Check Lock)就是一个典型,因此对多线程安全的理论分析就显得十分重要,当然这决不是说对多线程程序的测试就是不必要的。传统上,对多线程程序的分析是通过 分析操作之间可能的执行先后顺序,然而程序执行顺序十分复杂,它与硬件系统架构,编译器,缓存以及虚拟机的实现都有着很大的关系。仅仅为了分析多线程程序 就需要了解这么多底层知识确实不值得,况且当年选择学Java就是因为不用理会烦人的硬件和操作系统,这导致了许多Java程序员不愿也不能从理论上分析 多线程程序的正确性。虽然99%的Java程序员都知道DCL不对,但是如果让他们回答一些问题,DCL为什么不对?有什么修正方法?这个修正方法是正确 的吗?如果不正确,为什么不正确?对于此类问题,他们一脸茫然,或者回答也许吧,或者很自信但其实并没有抓住根本。

 

幸好现在还有另一条路可走,我们只需要利用几个基本的happen-before规则就能从理论上分析Java多线程程序的正确性,而且不需要涉及 到硬件和编译器的知识。接下来的部分,我会首先说明一下happen-before规则,然后使用happen-before规则来分析DCL,最后我以 我自己的例子来说明DCL的问题其实很常见,只是因为对DCL的过度关注反而忽略其问题本身,当然其忽略是有原因的,因为很多人并不知道DCL的问题到底 出在哪里。

 

 

Happen-Before规则

 

我们一般说一个操作happen-before另一个操作,这到底是什么意思呢?当说操作A happen-before操作B时,我们其实是在说在发生操作B之前,操作A对内存施加的影响能够被观测到。所谓“对内存施加的影响”就是指对变量的写 入,“被观测到”指当读取这个变量时能够得到刚才写入的值(如果中间没有发生其它的写入)。听起来很绕口?这就对了,请你保持耐心,举个例子来说明一下。 线程Ⅰ执行了操作A:x=3,线程Ⅱ执行了操作B:y=x。如果操作Ahappen-before操作B,线程Ⅱ在执行操作B之前就确定操作"x=3"被 执行了,它能够确定,是因为如果这两个操作之间没有任何对x的写入的话,它读取x的值将得到3,这意味着线程Ⅱ执行操作B会写入y的值为3。如果两个操作 之间还有对x的写入会怎样呢?假设线程Ⅲ在操作A和B之间执行了操作C: x=5,并且操作C和操作B之前并没有happen-before关系(后面我会说明时间上的先后并不一定导致happen-before关系)。这时线 程Ⅱ执行操作B会讲到x的什么值呢?3还是5?答案是两者皆有可能,这是因为happen-before关系保证一定 能够观测到前一个操作施加的内存影响,只有时间上的先后关系而并没有happen-before关系可能但并不保证 能观测前一个操作施加的内存影响。如果读到了值3,我们就说读到了“陈旧 ”的数据。正是多种可能性导致了多线程的不确定性和复杂性,但是要分析多线程的安全性,我们只能分析确定性部分,这就要求找出happen-before关系,这又得利用happen-before规则。

 

下面是我列出的三条非常重要的happen-before规则,利用它们可以确定两个操作之间是否存在happen-before关系。

  1. 同一个线程中,书写在前面的操作happen-before书写在后面的操作。这条规则是说,在单线程 中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的,这条规则也称为单线程规则 。这个规则多少说得有些简单了,考虑到控制结构和循环结构,书写在后面的操作可能happen-before书写在前面的操作,不过我想读者应该明白我的意思。
  2. 对锁的unlock操作happen-before后续的对同一个锁的lock操作。这里的“后续”指的是时间上的先后关系,unlock操作发 生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。但是仅仅是这条规则仍然不起任何作用,它 必须和下面这条规则联合起来使用才显得意义重大。这里关键条件是必须对“同一个锁”的lock和unlock。
  3. 如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。这条规则也称为传递规则。

 

现在暂时放下happen-before规则,先探讨一下“一个操作在时间上先于另一个操作发生”和“一个操作happen-before另一个操 作之间”的关系。两者有关联却并不相同。关联部分在第2条happen-before规则中已经谈到了,通常我们得假定一个时间上的先后顺序然后据此得出 happen-before关系。不同部分体现在,首先,一个操作在时间上先于另一个操作发生,并不意味着一个操作happen-before另一个操作 。看下面的例子:

Java代码  收藏代码
  1. public   void  setX( int  x) {  
  2.   this .x = x;                // (1)   
  3. }  
  4.   
  5. public   int  getX() {  
  6.   return  x;                  // (2)   
  7. }  

假设线程Ⅰ先执行setX方法,接着线程Ⅱ执行getX方法,在时间上线程Ⅰ的操作A:this.x = x先于线程Ⅱ的操作B:return x。但是操作A却并不happen-before操作B,让我们逐条检查三条happen-before规则。第1条规则在这里不适用,因为这时两个不同 的线程。第2条规则也不适用,因为这里没有任何同步块,也就没有任何lock和unlock操作。第3条规则必须基于已经存在的happen- before关系,现在没有得出任何happen-before关系,因此第三条规则对我们也任何帮助。通过检查这三条规则,我们就可以得出,操作A和操 作B之间没有happen-before关系。这意味着如果线程Ⅰ调用了setX(3),接着线程Ⅱ调用了getX(),其返回值可能不是3,尽管两个操 作之间没有任何其它操作对x进行写入,它可能返回任何一个曾经存在的值或者默认值0。“任何曾经存在的值”需要做点解释,假设在线程Ⅰ调用setX(3) 之前,还有别的线程或者就是线程Ⅰ还调用过setX(5), setX(8),那么x的曾经可能值为0, 5和8(这里假设setX是唯一能够改变x的方法),其中0是整型的默认值,用在这个例子中,线程Ⅱ调用getX()的返回值可能为0, 3, 5和8,至于到底是哪个值是不确定的。

 

现在将两个方法都设成同步的,也就是如下:

Java代码  收藏代码
  1. public   synchronized   void  setX( int  x) {  
  2.   this .x = x;                // (1)   
  3. }  
  4.   
  5. public   synchronized   int  getX() {  
  6.   return  x;                  // (2)   
  7. }  

做同样的假设,线程Ⅰ先执行setX方法,接着线程Ⅱ执行getX方法,这时就可以得出来,线程Ⅰ的操作A happen-before线程Ⅱ的操作B。下面我们来看如何根据happen-before规则来得到这个结论。由于操作A处于同步块中,操作A之后必 须定要发生对this锁的unlock操作,操作B也处于同步块中,操作B之前必须要发生对this锁的lock操作,根据假设unlock操作发生 lock操作之前,根据第2条happen-before规则,就得到unlock操作happen-before于lock操作;另外根据第1条 happen-before规则(单线程规则),操作A happen-before于unlock操作,lock操作happen-before于操作B;最后根据第3条happen-before规则(传递 规则),A -> unlock, unlock -> lock, lock -> B(这里我用->表示happen-before关系),有 A -> B,也就是说操作A happen-before操作B。这意味着如果线程Ⅰ调用了setX(3),紧接着线程Ⅱ调用了getX(),如果中间再没有其它线程改变x的值,那么 其返回值必定是3。

 

如果将两个方法的任何一个synchronized关键字去掉又会怎样呢?这时能不能得到线程Ⅰ的操作A happen-before线程Ⅱ的操作B呢?答案是得不到。这里因为第二条happen-before规则的条件已经不成立了,这时因为要么只有线程Ⅰ 的unlock操作(如果去掉getX的synchronized),要么只有线程Ⅱ的lock操作(如果去掉setX的synchronized关键 字)。这里也告诉我们一个原则,必须对同一个变量的 所有 读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的

 

其次,一个操作happen-before另一个操作 也并不意味着 一个操作在时间上先于另一个操作发生 。看下面的例子:

Java代码  收藏代码
  1. x =  3 ;      ( 1 )  
  2. y = 2 ;      ( 2 )  

同一个线程执行上面的两个操作,操作A:x = 3和操作B:y = 2。根据单线程规则,操作A happen-before操作B,但是操作A却不一定在时间上先于操作B发生,这是因为编译器的重新排序等原因,操作B可能在时间上后于操作B发生。这 个例子也说明了,分析操作上先后顺序是多么地不靠谱,它可能完全违反直观感觉。

 

最后,一个操作和另一个操作必定存在某个顺序,要么一个操作或者是先于或者是后于另一个操作,或者与两个操作同时发生。同时发生是完全可能存在的, 特别是在多CPU的情况下。而两个操作之间却可能没有happen-before关系,也就是说有可能发生这样的情况,操作A不happen- before操作B,操作B也不happen-before操作A,用数学上的术语happen-before关系是个偏序关系。两个存在happen- before关系的操作不可能同时发生,一个操作A happen-before操作B,它们必定在时间上是完全错开的,这实际上也是同步的语义之一(独占访问)。

 

在运用happen-before规则分析DCL之前,有必要对“操作”澄清一下,在前面的叙述中我一直将语句是操作的同义词,这么讲是不严格的, 严格上来说这里的操作应该是指单个虚拟机的指令,如moniterenter, moniterexit, add, sub, store, load等。使用语句来代表操作并不影响我们的分析,下面我仍将延续这一传统,并且将直接用语句来代替操作。唯一需要注意的是单个语句实际上可能由多个指 令组成,比如语句x=i++由两条指令(inc和store)组成。现在我们已经完成了一切理论准备,你一定等不及要动手开干了(我都写烦了)。

 

 

利用Happen-Before规则分析DCL

 

下面是一个典型的使用DCL的例子:

 

Java代码  收藏代码
  1. public   class  LazySingleton {  
  2.     private   int  someField;  
  3.       
  4.     private   static  LazySingleton instance;  
  5.       
  6.     private  LazySingleton() {  
  7.         this .someField =  new  Random().nextInt( 200 )+ 1 ;          // (1)   
  8.     }  
  9.       
  10.     public   static  LazySingleton getInstance() {  
  11.         if  (instance ==  null ) {                                // (2)   
  12.             synchronized (LazySingleton. class ) {                // (3)   
  13.                 if  (instance ==  null ) {                        // (4)   
  14.                     instance = new  LazySingleton();            // (5)   
  15.                 }  
  16.             }  
  17.         }  
  18.         return  instance;                                       // (6)   
  19.     }  
  20.       
  21.     public   int  getSomeField() {  
  22.         return   this .someField;                                 // (7)   
  23.     }  
  24. }  

 

为了分析DCL,我需要预先陈述上面程序运行时几个事实:

  1. 语句(5)只会被执行一次,也就是LazySingleton只会存在一个实例,这是由于它和语句(4)被放在同步块中被执行的缘故,如果去掉语句(3)处的同步块,那么这个假设便不成立了。
  2. instance只有两种“曾经可能存在”的值,要么为null,也就是初始值,要么为执行语句(5)时构造的对象引用。这个结论由事实1很容易推出来。
  3. getInstance()总是返回非空值,并且每次调用返回相同的引用。如果getInstance()是初次调用,它会执行语句(5)构造一 个LazySingleton实例并返回,如果getInstance()不是初次调用,如果不能在语句(2)处检测到非空值,那么必定将在语句(4)处 就能检测到instance的非空值,因为语句(4)处于同步块中,对instance的写入--语句(5)也处于同一个同步块中。

有读者可能要问了,既然根据第3条事实getInstance()总是返回相同的正确的引用,为什么还说DCL有问题呢?这里的关键是 尽管得到了LazySingleton的正确引用,但是却有可能访问到其成员变量 不正确值 ,具体来说LazySingleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行 为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。为也说明这种情况理论上有可能发生,我们只需要说明语句 (1)和语句(7)并不存在happen-before关系。

 

假设线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法, 我们要说明的是线程Ⅰ的语句(1)并不happen-before线程Ⅱ的语句(7)。线程Ⅱ在执行getInstance()方法的语句(2)时,由于 对instance的访问并没有处于同步块中,因此线程Ⅱ可能观察到也可能观察不到线程Ⅰ在语句(5)时对instance的写入,也就是说 instance的值可能为空也可能为非空。我们先假设instance的值非空,也就观察到了线程Ⅰ对instance的写入,这时线程Ⅱ就会执行语句 (6)直接返回这个instance的值,然后对这个instance调用getSomeField()方法,该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们无法利用第1条和第2条happen-before规则得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系,这说 明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语 句(1)处对someFiled写入的值,这就是DCL的问题所在。很荒谬,是吧?DCL原本是为了逃避同步,它达到了这个目的,也正是因为如此,它最终 受到惩罚,这样的程序存在严重的bug,虽然这种bug被发现的概率绝对比中彩票的概率还要低得多,而且是转瞬即逝,更可怕的是,即使发生了你也不会想到 是DCL所引起的。

 

前面我们说了,线程Ⅱ在执行语句(2)时也有可能观察空值,如果是种情况,那么它需要进入同步块,并执行语句(4)。在语句(4)处线程Ⅱ还能够读 到instance的空值吗?不可能。这里因为这时对instance的写和读都是发生在同一个锁确定的同步块中,这时读到的数据是最新的数据。为也加深 印象,我再用happen-before规则分析一遍。线程Ⅱ在语句(3)处会执行一个lock操作,而线程Ⅰ在语句(5)后会执行一个unlock操 作,这两个操作都是针对同一个锁--LazySingleton.class,因此根据第2条happen-before规则,线程Ⅰ的unlock操作 happen-before线程Ⅱ的lock操作,再利用单线程规则,线程Ⅰ的语句(5) -> 线程Ⅰ的unlock操作,线程Ⅱ的lock操作 -> 线程Ⅱ的语句(4),再根据传递规则,就有线程Ⅰ的语句(5) -> 线程Ⅱ的语句(4),也就是说线程Ⅱ在执行语句(4)时能够观测到线程Ⅰ在语句(5)时对LazySingleton的写入值。接着对返回的 instance调用getSomeField()方法时,我们也能得到线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7),这表明这时getSomeField能够得到正确的值。但是仅仅是这种情况的正确性并不妨碍DCL的不正确性,一个程序的正确性必须 在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。

 

对DCL的分析也告诉我们一条经验原则,对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。

 

再稍微对DCL探讨一下,这个例子中的LazySingleton是一个不变类,它只有get方法而没有set方法。由对DCL的分析我们知道,即使一个对象是不变的,在不同的线程中它的同一个方法也可能返回不同的值 。之所以会造成这个问题,是因为LazySingleton实例没有被安全发布,所谓“被安全的发布”是指所有的线程应该在同步块中获得这个实例。这样我们又得到一个经验原则,即使对于不可变对象,它也必须被安全的发布,才能被安全地共享。 所谓“安全的共享”就是说不需要同步也不会遇到数据竞争的问题。在Java5或以后,将someField声明成final的,即使它不被安全的发布,也能被安全地共享,而在Java1.4或以前则必须被安全地发布。

 

关于DCL的修正

 

既然理解了DCL的根本原因,或许我们就可以修正它。

 

既然原因是线程Ⅱ执行getInstance()可能根本没有在同步块中执行,那就将整个方法都同步吧。这个毫无疑问是正确的,但是这却回到最初的 起点(返朴归真了),也完全违背了DCL的初衷,尽可能少的减少同步。虽然这不能带任何意义,却也说明一个道理,最简单的往往是最好的。

 

如果我们尝试不改动getInstance()方法,而是在getSomeField()上做文章,那么首先想到的应该是将getSomeField设置成同步,如下所示:

 

Java代码  收藏代码
  1. public   synchronized   int  getSomeField() {  
  2.     return   this .someField;                                 // (7)   
  3. }  

 

这种修改是不是正确的呢?答案是不正确。这是因为,第2条happen-before规则的前提条件并不成立。语句(5)所在同步块和语句(7)所在同步块并不是使用同一个锁。像下面这样修改才是对的:

Java代码  收藏代码
  1. public   int  getSomeField() {  
  2.     synchronized (LazySingleton. class ) {  
  3.         return   this .someField;  
  4.     }  
  5. }  

但是这样的修改虽然能保证正确性却不能保证高性能。因为现在每次读访问getSomeField()都要同步,如果使用简单的方法,将整个 getInstance()同步,只需要在getInstance()时同步一次,之后调用getSomeField()就不需要同步了。另外 getSomeField()方法也显得很奇怪,明明是要返回实例变量却要使用Class锁。这也再次验证了一个道理,简单的才是好的。

 

好了,由于我的想象力有限,我能想到的修正也就仅限于此了,让我们来看看网上提供的修正吧。

 

首先看Lucas Lee的修正(这里 是原帖):

Java代码  收藏代码
  1. private   static  LazySingleton instance;  
  2. private   static   int  hasInitialized =  0 ;  
  3.       
  4. public   static  LazySingleton getInstance() {  
  5.     if  (hasInitialized ==  0 ) {                                           // (4)   
  6.         synchronized (LazySingleton. class ) {                          // (5)   
  7.             if  (instance ==  null ) {                                  // (6)   
  8.                 instance = new  LazySingleton();                      // (7)   
  9.                 hasInitialized = 1 ;  
  10.             }  
  11.         }  
  12.     }  
  13.     return  instance;                                                 // (8)   
  14. }  

如果你明白我前面所讲的,那么很容易看出这里根本就是一个伪修正,线程Ⅱ仍然完全有可能在非同步状态下返回instance。Lucas Lee的理由是对int变量的赋值是原子的,但实际上对instance的赋值也是原子的,Java语言规范规定对任何引用变量和基本变量的赋值都是原子 的,除了long和double以外。使用hasInitialized==0和instance==null来判断LazySingleton有没有初 始化没有任何区别。Lucas Lee对http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 中的最后一个例子有些误解,里面的计算hashCode的例子之所以是正确的,是因为它返回的是int而不是对象的引用,因而不存在访问到不正确成员变量值的问题。

 

neuzhujf的修正:

Java代码  收藏代码
  1. public   static  LazySingleton getInstance() {  
  2.     if  (instance ==  null ) {                                          // (4)   
  3.         synchronized (LazySingleton. class ) {                          // (5)   
  4.             if  (instance ==  null ) {                                  // (6)   
  5.                 LazySingleton localRef = new  LazySingleton();  
  6.                 instance = localRef;                        // (7)   
  7.             }  
  8.         }  
  9.     }  
  10.     return  instance;                                                 // (8)   
  11. }  

这里只是引入了一个局部变量,这也容易看出来只是一个伪修正,如果你弄明白了我前面所讲的。

 

既然提到DCL,就不得不提到一个经典的而且正确的修正。就是使用一个static holder,kilik在回复中给出了这样的一个修正。由于这里一种完全不同的思路,与我这里讲的内容也没有太大的关系,暂时略了吧。另外一个修正是使 用是threadlocal,都可以参见这篇文章

 

步入Java5

 

前面所讲的都是基于Java1.4及以前的版本,java5对内存模型作了重要的改 动,其中最主要的改动就是对volatile和final语义的改变。本文使用的happen-before规则实际上是从Java5中借鉴而来,然后再 移花接木到Java1.4中,因此也就不得不谈下Java5中的多线程了。

 

在java 5中多增加了一条happen-before规则:

  • 对volatile字段的写操作happen-before后续的对同一个字段的读操作。

利用这条规则我们可以将instance声明为volatile,即:

Java代码  收藏代码
  1. private   volatile   static  LazySingleton instance;  

 根据这条规则,我们可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。

 

在java5之前对final字段的同步语义和其它变量没有什么区别,在java5中,final变量一旦在构造函数中设置完成(前提是在构造函数 中没有泄露this引用),其它线程必定会看到在构造函数中设置的值。而DCL的问题正好在于看到对象的成员变量的默认值,因此我们可以将 LazySingleton的someField变量设置成final,这样在java5中就能够正确运行了。

 

 

遭遇同样错误

 

在Java世界里,框架似乎做了很多事情来隐藏多线程,以至于很多程序员认为不再需要关注多线程了。 这实际上是个陷阱,这它只会使我们对多线程程序的bug反应迟钝。大部分程序员(包括我)都不 会特别留意类文档中的线程不安全警告,自己写程序时也不会考虑将该类是否线程安全写入文档 中。做个测试,你知道java.text.SimpleDateFormat不是线程安全的吗?如果你不知道,也不要感到奇怪,我也是在《Java Concurrent In Practice 》这书中才看到的。

 

现在我们已经明白了DCL中的问题,很多人都只认为这只不过是不切实际的理论者整天谈论的话题,殊不知这样的错误其实很常见。我就犯过,下面是从我同一个项目中所写的代码中摘录出来的,读者也不妨拿此来检验一下自己,你自己犯过吗?即使没有,你会毫不犹豫的这样写吗?

 

第一个例子:

Java代码  收藏代码
  1. public   class  TableConfig {  
  2.     //....   
  3.     private  FieldConfig[] allFields;  
  4.       
  5.     private   transient  FieldConfig[] _editFields;  
  6.   
  7.     //....   
  8.       
  9.     public  FieldConfig[] getEditFields() {  
  10.         if  (_editFields ==  null ) {  
  11.             List<FieldConfig> editFields = new  ArrayList<FieldConfig>();  
  12.             for  ( int  i =  0 ; i < allFields.length; i++) {  
  13.                 if  (allFields[i].editable) editFields.add(allFields[i]);  
  14.             }  
  15.             _editFields = editFields.toArray(new  FieldConfig[editFields.size()]);  
  16.         }  
  17.         return  _editFields;  
  18.     }  
  19. }  

这里缓存了TableConfig的_editFields,免得以后再取要重新遍历allFields。这里存在和DCL同样的问题,_editFields数组的引用可能是正确的值,但是数组成员却可能null! 与DCL不同的是 ,由于对_editFields的赋值没有同步,它可能被赋值多次,但是在这里没有问题,因为每次赋值虽然其引用值不同,但是其数组成员是相同的,对于我 的业务来说,它们都等价的。由于我的代码是要用在java1.4中,因此唯一的修复方法就是将整个方法声明为同步。

 

第二个例子:

Java代码  收藏代码
  1. private  Map selectSqls =  new  HashMap();  
  2.   
  3. public  Map executeSelect( final  TableConfig tableConfig, Map keys) {  
  4.     if  (selectSqls.get(tableConfig.getId()) ==  null ) {  
  5.         selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));  
  6.     }  
  7.     PreparedSql psql = (PreparedSql) selectSqls.get(tableConfig.getId());  
  8.   
  9.     List result = executeSql(...);  
  10.          
  11.        return  result.isEmpty() ?  null  : (Map) result.get( 0 );  
  12.    }  
 

上面的代码用constructSelectSql()方法来动态构造SQL语句,为了避免构造的开销,将先前构造的结果缓存在 selectSqls这个Map中,下次直接从缓存取就可以了。显然由于没有同步,这段代码会遭遇和DCL同样的问题,虽然 selectSqls.get(...)可能能够返回正确的引用,但是却有可能返回该引用成员变量的非法值。另外selectSqls使用了非同步的 Map,并发调用时可能会破坏它的内部状态,这会造成严重的后果,甚至程序崩溃。可能的修复就是将整个方法声明为同步:

 

 

Java代码  收藏代码
  1. public   synchronized  Map executeSelect( final  TableConfig tableConfig, Map keys)  {  
  2.     // ....   
  3.    }  

但是这样马上会遭遇吞吐量的问题,这里在同步块执行了数据库查询,执行数据库查询是是个很慢的操作,这会导致其它线程执行同样的操作时造成不必要的等待,因此较好的方法是减少同步块的作用域,将数据库查询操作排除在同步块之外:

Java代码  收藏代码
  1. public  Map executeSelect( final  TableConfig tableConfig, Map keys)  {  
  2.     PreparedSql psql = null ;  
  3.     synchronized ( this ) {  
  4.     if  (selectSqls.get(tableConfig.getId()) ==  null ) {  
  5.         selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));  
  6.     }  
  7.     psql = (PreparedSql) selectSqls.get(tableConfig.getId());  
  8.     }  
  9.   
  10.     List result = executeSql(...);  
  11.       
  12.     return  result.isEmpty() ?  null  : (Map) result.get( 0 );  
  13. }  

现在情况已经改善了很多,毕竟我们将数据库查询操作拿到同步块外面来了。但是仔细观察会发现将this作为同步锁并不是一个好主意,同步块的目的是 保证从selectSqls这个Map中取到的是一致的对象,因此用selectSqls作为同步锁会更好,这能够提高性能。这个类中还存在很多类似的方 法executeUpdate,executeInsert时,它们都有自己的sql缓存,如果它们都采用this作为同步锁,那么在执行 executeSelect方法时需要等待executeUpdate方法,而这种等待原本是不必要的。使用细粒度的锁,可以消除这种等待,最后得到修改 后的代码:

Java代码  收藏代码
  1. private  Map selectSqls = Collections.synchronizedMap( new  HashMap())  
  2.    public  Map executeSelect( final  TableConfig tableConfig, Map keys)  {  
  3.     PreparedSql psql = null ;  
  4.     synchronized (selectSqls) {  
  5.         if  (selectSqls.get(tableConfig.getId()) ==  null ) {  
  6.             selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));  
  7.         }  
  8.         psql = (PreparedSql) selectSqls.get(tableConfig.getId());  
  9.     }  
  10.   
  11.     List result = executeSql(...);  
  12.          
  13.        return  result.isEmpty() ?  null  : (Map) result.get( 0 );  
  14.    }  

我对selectSqls使用了同步Map,如果它只被这个方法使用,这就不是必须的。作为一种防范措施,虽然这会稍微降低性能,即便当它被其它方 法使用了也能够保护它的内部结构不被破坏。并且由于Map的内部锁是非竞争性锁,根据官方说法,这对性能影响很小,可以忽略不计。这里我有意无意地提到了 编写高性能的两个原则,尽量减少同步块的作用域,以及使用细粒度的锁 ,关于细粒度锁的最经典例子莫过于读写锁了。这两个原则要慎用,除非你能保证你的程序是正确的。

 

 

结束语

 

在这篇文章中我主要讲到happen-before规则,并运用它来分析DCL问题,最后我用例子来说明DCL问题并不只是理论上的讨论,在实际程 序中其实很常见。我希望读者能够明白用happen-before规则比使用时间的先后顺序来分析线程安全性要有效得多,作为对比,你可以看看这篇经典的文章 中是如何分析DCL的线程安全性的。它是否讲明白了呢?如果它讲明白了,你是否又能理解?我想答案很可能是否定的,不然的话就不会出现这么多对DCL的误 解了。当然我也并不是说要用happen-before规则来分析所有程序的线程安全性,如果你试着分析几个程序就会发现这是件很困难的事,因为这个规则 实在是太底层了,要想更高效的分析程序的线程安全性,还得总结和利用了一些高层的经验规则。关于这些经验规则,我在文中也谈到了一些,很零碎也不完全。

分享到:
评论

相关推荐

    Java利用happen-before规则如何实现共享变量的同步操作详解

    主要给大家介绍了关于Java利用happen-before规则实现共享变量的同步操作的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用java具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

    第29讲Java内存模型中的happen-before1

    它的具体表现形式,包括但远不止是我们直觉中的 synchronized、volatile、lock 操作顺序等方面,例如:线程内执行的每个操作,都保证 happ

    Crashes Happen - Downtime Won't with Data Guard

    Crashes Happen - Downtime Won't with Data Guard

    深入浅出了解happens-before原则

    一提到happens-before原则,就让人有点“丈二和尚摸不着头脑”。这个涵盖了整个JMM中可见性原则的规则,究竟如何理解,把我个人一些理解记录下来。下面可以和小编一起学习

    volatile与happens-before的关系与内存一致性错误

    本文讲了Java并发编程中volatile变量、happens-before与内存一致性错误,下面来和小编一起学习下如何解决

    make-fetch-happen:使获取发生在npm

    $ npm install --save make-fetch-happen 目录 fetch.defaults minipass-fetch选项 make-fetch-happen选项 opts.cacheManager opts.cache opts.proxy opts.noProxy opts.ca, opts.cert, opts.key opts....

    leetcode数组下标大于间距-happen-code:进入Leetcode~

    happen-code Get into Leetcode~ Code List 动态规划 动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式...

    ERP Make it Happen

    ERP Make it Happen ERP Make it Happen ERP Make it Happen ERP Make it Happen

    Project Make It Happen-crx插件

    Project Make It Happen Extension是一个活动日历,可以帮助我们的团队随时了解最新事件,推送通知和每日激励中心。 通过单击第一个选项卡上包含的链接登录到您的后台。 单击第一个选项卡上的日历将跳至第二个选项卡...

    make-it-happen-jsonforms:使其成为JSON表单的示例

    请改为使用 。 让它发生的例子 使其成为示例 入门 首先,您可以简单地克隆此存储库并安装所有必需的依赖项。 先决条件 您需要git来克隆MiHexample存储库。 您可以从获得git。 我们还使用了许多node.js工具来初始化...

    make-fetch-happen:加入失败者,我们正在提出要求!

    安装$ npm install --save make-fetch-happen 目录例子const fetch = require ( 'make-fetch-happen' ) . defaults ( { cacheManager : './my-cache' // path where cache will be written (and read)} )fetch ( '...

    John Wiley & Sons - ERP - Making It Happen

    有关ERP和SAP的使用中的经典案例,语言简单易懂,非常利于ERP和SAP的学习。

    java反编译泄露源码-interview-:阿里、腾讯、百度、华为、京东、搜狗和滴滴最新面试题汇集

    java反编译 泄露源码 阿里、腾讯、百度、华为、京东、搜狗和滴滴最新面试题汇集 最后更新于20180513 ...2、简述happen-before规则; 3、JUC和Object;Monitor机制区别是什么;简述AQS原理; 4、简述

    MS-DOS 5.0

    do before running Setup and information about using Windows, memory, and hardware with MS-DOS 5.0. It also contains updates to the User's Guide and Reference. For information about using ...

    Audio Visualizer v2.4.txt

    - Detect beats before they happen. - Create games like Rockband or AudioSurf. - Completely Customizable. - Easy to use. - Mobile Friendly - Well documented. - Tutorials provided. - 17 ...

    NGUI Next-Gen UI v3.7.8.unitypackage

    - NEW: Added a validator delegate to the toggle class for when you want to add custom code to prevent state changes before they happen. - FIX: Some more compile fixes for newer Unity 5 versions. - FIX...

    Making_Things_Happen

    In the updated edition of this critically acclaimed and bestselling book Microsoft project veteran Scott Berkun offers a collection of essays on field tested philosophies and strategies for defining ...

    Java 高并发三:Java内存模型和线程安全详解

    本文主要介绍Java高并发内存模型和线程安全的资料,这里整理详细的资料及1.原子性 2.有序性 3.可见性 4.Happen-Before 5.线程安全的概念,有需要的小伙伴可以参考下

    项目实现它「Project Make It Happen」-crx插件

    Project Make It Happen Extension是一个活动日历,可以帮助我们的团队随时了解最新事件,推送通知和每日激励中心。 通过单击第一个选项卡上包含的链接登录到您的后台。 单击第一个选项卡上的日历将跳至第二个选项卡...

Global site tag (gtag.js) - Google Analytics