`

用happen-before规则重新审视DCL(转载)

 
阅读更多
详见:http://www.iteye.com/topic/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规则来分析所有程序的线程安全性,如果你试着分析几个程序就会发现这是件很困难的事,因为这个规则实在是太底层了,要想更高效的分析程序的线程安全性,还得总结和利用了一些高层的经验规则。关于这些经验规则,我在文中也谈到了一些,很零碎也不完全。
分享到:
评论

相关推荐

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

    在 JMM 中,`happen-before` 规则是定义这些同步特性的重要概念。它是一种偏序关系,确保了在程序中两个操作之间的执行顺序,并且保证了前一个操作的结果对后一个操作可见。以下是一些 `happen-before` 的典型规则:...

    深入浅出了解happens-before原则

    "深入浅出了解happens-before原则" 在 Java 内存模型(JMM)中,happens-before 原则是一个核心概念,它规定了多线程环境下的可见性原则。该原则的核心思想是:两个操作之间的 happens-before 关系,并不意味着前一...

    Crashes Happen - Downtime Won't with Data Guard

    "Crashes Happen - Downtime Won't with Data Guard"这个主题强调了即使在系统崩溃的情况下,通过Data Guard也能避免停机时间,确保业务连续性。 Data Guard的核心是通过创建一个或多个备用数据库(standby ...

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

    在Java并发编程中,理解和掌握volatile关键字以及happens-before原则对于解决内存一致性错误至关重要。内存一致性错误通常出现在多线程环境下,当不同线程访问共享变量时,由于缓存和处理器优化,可能导致数据不一致...

    volatile可见性的一些认识和论证

    本文探讨了Java中volatile关键词的使用和可见性问题,我们将结合JVM内存模型和happen-before概念,来讨论volatile的使用场景和例子。 一、happen-before happen-before是一个可见性原则,定义了Java语言中操作的...

    并发编程基础知识,java内存模型及多线程、volatile

    Happen-Before规则是一组用于保证线程之间执行顺序的规则,主要包括: - 程序顺序原则:在一个线程内部保证语义的串行性。 - volatile规则:volatile变量的写操作先于任何读操作。 - 锁规则:解锁操作必然发生在...

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

    JSR133中文版,JSR133中文版

    Happen-Before内存模型是JSR-133中定义的一种内存模型,它描述了操作之间的顺序关系。这种内存模型可以用来确保在多线程环境下,操作的执行顺序是正确的。 JSR-133是Java语言中非常重要的规范,它定义了Java语言的...

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

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

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

    例如,线程的启动操作Happen-Before该线程的任何读写操作,`synchronized`块的结束Happen-Before同一锁的下次获取,以及对`volatile`变量的写操作Happen-Before其他线程对这个变量的读操作。 线程安全是指在多线程...

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

    JVM面试题分享给需要的同学.zip

    6JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存 7.简单说说你了解的类加载器,可以打破双亲委派么,怎么打破 8.说说你知道的几种主要的JVM参数 9.怎么打出线程栈信息 10....

    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 ( '...

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

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

    JAVA并发编程阿里巴巴.pptx

    - **Happen-Before规则**:确保代码执行的有序性,从而保证了数据的可见性。 - 程序顺序规则:代码按照程序顺序执行。 - 监视器锁定规则:对一个监视器的解锁一定发生在后续对同一监视器加锁之前。 - volatile...

    John Wiley & Sons - ERP - Making It Happen

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

Global site tag (gtag.js) - Google Analytics