- 浏览: 108813 次
- 性别:
- 来自: 北京
文章分类
- 全部博客 (75)
- JVM (22)
- 数据结构 (11)
- java 基础 (16)
- gc (6)
- jmock (1)
- Google (2)
- MapReduce (1)
- Memory (2)
- 算法 (2)
- cglib (1)
- jdk (3)
- 虚拟机 (3)
- 安全 (2)
- 多线程 (1)
- 工作 (1)
- 生活 (1)
- MongoDB (2)
- Hadoop (4)
- HDFS (2)
- cms (2)
- Spring (1)
- 网络协议 (1)
- GitHub (1)
- MYSQL 调优和使用必读(转) (1)
- 分布式 (2)
- Big Data (0)
- 技术Blog (1)
- Hbase (2)
- Zookeeper (1)
- paper (0)
最新评论
-
lzc_java:
Java线程安全兼谈DCL -
select*from爱:
it's nice
IT业薪水大揭秘
转载自 ---- 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关系。
- 同一个线程中,书写在前面的操作happen-before书写在后面的操作。这条规则是说,在单线程 中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的,这条规则也称为单线程规则 。这个规则多少说得有些简单了,考虑到控制结构和循环结构,书写在后面的操作可能happen-before书写在前面的操作,不过我想读者应该明白我的意思。
- 对锁的unlock操作happen-before后续的对同一个锁的lock操作。这里的“后续”指的是时间上的先后关系,unlock操作发 生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。但是仅仅是这条规则仍然不起任何作用,它 必须和下面这条规则联合起来使用才显得意义重大。这里关键条件是必须对“同一个锁”的lock和unlock。
- 如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。这条规则也称为传递规则。
现在暂时放下happen-before规则,先探讨一下“一个操作在时间上先于另一个操作发生”和“一个操作happen-before另一个操 作之间”的关系。两者有关联却并不相同。关联部分在第2条happen-before规则中已经谈到了,通常我们得假定一个时间上的先后顺序然后据此得出 happen-before关系。不同部分体现在,首先,一个操作在时间上先于另一个操作发生,并不意味着一个操作happen-before另一个操作 。看下面的例子:
假设线程Ⅰ先执行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,至于到底是哪个值是不确定的。
现在将两个方法都设成同步的,也就是如下:
- public synchronized void setX( int x) {
- this .x = x; // (1)
- }
- public synchronized int getX() {
- return x; // (2)
- }
做同样的假设,线程Ⅰ先执行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另一个操作 也并不意味着 一个操作在时间上先于另一个操作发生 。看下面的例子:
同一个线程执行上面的两个操作,操作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的例子:
- public class LazySingleton {
- private int someField;
- private static LazySingleton instance;
- private LazySingleton() {
- this .someField = new Random().nextInt( 200 )+ 1 ; // (1)
- }
- public static LazySingleton getInstance() {
- if (instance == null ) { // (2)
- synchronized (LazySingleton. class ) { // (3)
- if (instance == null ) { // (4)
- instance = new LazySingleton(); // (5)
- }
- }
- }
- return instance; // (6)
- }
- public int getSomeField() {
- return this .someField; // (7)
- }
- }
为了分析DCL,我需要预先陈述上面程序运行时几个事实:
- 语句(5)只会被执行一次,也就是LazySingleton只会存在一个实例,这是由于它和语句(4)被放在同步块中被执行的缘故,如果去掉语句(3)处的同步块,那么这个假设便不成立了。
- instance只有两种“曾经可能存在”的值,要么为null,也就是初始值,要么为执行语句(5)时构造的对象引用。这个结论由事实1很容易推出来。
- 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设置成同步,如下所示:
这种修改是不是正确的呢?答案是不正确。这是因为,第2条happen-before规则的前提条件并不成立。语句(5)所在同步块和语句(7)所在同步块并不是使用同一个锁。像下面这样修改才是对的:
- public int getSomeField() {
- synchronized (LazySingleton. class ) {
- return this .someField;
- }
- }
但是这样的修改虽然能保证正确性却不能保证高性能。因为现在每次读访问getSomeField()都要同步,如果使用简单的方法,将整个 getInstance()同步,只需要在getInstance()时同步一次,之后调用getSomeField()就不需要同步了。另外 getSomeField()方法也显得很奇怪,明明是要返回实例变量却要使用Class锁。这也再次验证了一个道理,简单的才是好的。
好了,由于我的想象力有限,我能想到的修正也就仅限于此了,让我们来看看网上提供的修正吧。
首先看Lucas Lee的修正(这里 是原帖):
- private static LazySingleton instance;
- private static int hasInitialized = 0 ;
- public static LazySingleton getInstance() {
- if (hasInitialized == 0 ) { // (4)
- synchronized (LazySingleton. class ) { // (5)
- if (instance == null ) { // (6)
- instance = new LazySingleton(); // (7)
- hasInitialized = 1 ;
- }
- }
- }
- return instance; // (8)
- }
如果你明白我前面所讲的,那么很容易看出这里根本就是一个伪修正,线程Ⅱ仍然完全有可能在非同步状态下返回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的修正:
- public static LazySingleton getInstance() {
- if (instance == null ) { // (4)
- synchronized (LazySingleton. class ) { // (5)
- if (instance == null ) { // (6)
- LazySingleton localRef = new LazySingleton();
- instance = localRef; // (7)
- }
- }
- }
- return instance; // (8)
- }
这里只是引入了一个局部变量,这也容易看出来只是一个伪修正,如果你弄明白了我前面所讲的。
既然提到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,即:
根据这条规则,我们可以得到,线程Ⅰ的语句(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中的问题,很多人都只认为这只不过是不切实际的理论者整天谈论的话题,殊不知这样的错误其实很常见。我就犯过,下面是从我同一个项目中所写的代码中摘录出来的,读者也不妨拿此来检验一下自己,你自己犯过吗?即使没有,你会毫不犹豫的这样写吗?
第一个例子:
- public class TableConfig {
- //....
- private FieldConfig[] allFields;
- private transient FieldConfig[] _editFields;
- //....
- public FieldConfig[] getEditFields() {
- if (_editFields == null ) {
- List<FieldConfig> editFields = new ArrayList<FieldConfig>();
- for ( int i = 0 ; i < allFields.length; i++) {
- if (allFields[i].editable) editFields.add(allFields[i]);
- }
- _editFields = editFields.toArray(new FieldConfig[editFields.size()]);
- }
- return _editFields;
- }
- }
这里缓存了TableConfig的_editFields,免得以后再取要重新遍历allFields。这里存在和DCL同样的问题,_editFields数组的引用可能是正确的值,但是数组成员却可能null! 与DCL不同的是 ,由于对_editFields的赋值没有同步,它可能被赋值多次,但是在这里没有问题,因为每次赋值虽然其引用值不同,但是其数组成员是相同的,对于我 的业务来说,它们都等价的。由于我的代码是要用在java1.4中,因此唯一的修复方法就是将整个方法声明为同步。
第二个例子:
- private Map selectSqls = new HashMap();
- public Map executeSelect( final TableConfig tableConfig, Map keys) {
- if (selectSqls.get(tableConfig.getId()) == null ) {
- selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));
- }
- PreparedSql psql = (PreparedSql) selectSqls.get(tableConfig.getId());
- List result = executeSql(...);
- return result.isEmpty() ? null : (Map) result.get( 0 );
- }
上面的代码用constructSelectSql()方法来动态构造SQL语句,为了避免构造的开销,将先前构造的结果缓存在 selectSqls这个Map中,下次直接从缓存取就可以了。显然由于没有同步,这段代码会遭遇和DCL同样的问题,虽然 selectSqls.get(...)可能能够返回正确的引用,但是却有可能返回该引用成员变量的非法值。另外selectSqls使用了非同步的 Map,并发调用时可能会破坏它的内部状态,这会造成严重的后果,甚至程序崩溃。可能的修复就是将整个方法声明为同步:
但是这样马上会遭遇吞吐量的问题,这里在同步块执行了数据库查询,执行数据库查询是是个很慢的操作,这会导致其它线程执行同样的操作时造成不必要的等待,因此较好的方法是减少同步块的作用域,将数据库查询操作排除在同步块之外:
- public Map executeSelect( final TableConfig tableConfig, Map keys) {
- PreparedSql psql = null ;
- synchronized ( this ) {
- if (selectSqls.get(tableConfig.getId()) == null ) {
- selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));
- }
- psql = (PreparedSql) selectSqls.get(tableConfig.getId());
- }
- List result = executeSql(...);
- return result.isEmpty() ? null : (Map) result.get( 0 );
- }
现在情况已经改善了很多,毕竟我们将数据库查询操作拿到同步块外面来了。但是仔细观察会发现将this作为同步锁并不是一个好主意,同步块的目的是 保证从selectSqls这个Map中取到的是一致的对象,因此用selectSqls作为同步锁会更好,这能够提高性能。这个类中还存在很多类似的方 法executeUpdate,executeInsert时,它们都有自己的sql缓存,如果它们都采用this作为同步锁,那么在执行 executeSelect方法时需要等待executeUpdate方法,而这种等待原本是不必要的。使用细粒度的锁,可以消除这种等待,最后得到修改 后的代码:
- private Map selectSqls = Collections.synchronizedMap( new HashMap())
- public Map executeSelect( final TableConfig tableConfig, Map keys) {
- PreparedSql psql = null ;
- synchronized (selectSqls) {
- if (selectSqls.get(tableConfig.getId()) == null ) {
- selectSqls.put(tableConfig.getId(), constructSelectSql(tableConfig));
- }
- psql = (PreparedSql) selectSqls.get(tableConfig.getId());
- }
- List result = executeSql(...);
- return result.isEmpty() ? null : (Map) result.get( 0 );
- }
我对selectSqls使用了同步Map,如果它只被这个方法使用,这就不是必须的。作为一种防范措施,虽然这会稍微降低性能,即便当它被其它方 法使用了也能够保护它的内部结构不被破坏。并且由于Map的内部锁是非竞争性锁,根据官方说法,这对性能影响很小,可以忽略不计。这里我有意无意地提到了 编写高性能的两个原则,尽量减少同步块的作用域,以及使用细粒度的锁 ,关于细粒度锁的最经典例子莫过于读写锁了。这两个原则要慎用,除非你能保证你的程序是正确的。
结束语
在这篇文章中我主要讲到happen-before规则,并运用它来分析DCL问题,最后我用例子来说明DCL问题并不只是理论上的讨论,在实际程 序中其实很常见。我希望读者能够明白用happen-before规则比使用时间的先后顺序来分析线程安全性要有效得多,作为对比,你可以看看这篇经典的文章 中是如何分析DCL的线程安全性的。它是否讲明白了呢?如果它讲明白了,你是否又能理解?我想答案很可能是否定的,不然的话就不会出现这么多对DCL的误 解了。当然我也并不是说要用happen-before规则来分析所有程序的线程安全性,如果你试着分析几个程序就会发现这是件很困难的事,因为这个规则 实在是太底层了,要想更高效的分析程序的线程安全性,还得总结和利用了一些高层的经验规则。关于这些经验规则,我在文中也谈到了一些,很零碎也不完全。
发表评论
-
Java基础 之软引用、弱引用、虚引用 ·[转载]
2012-06-07 18:13 11391、概述 在JDK1.2以前的版本中,当一个对象不 ... -
Java中常用的加密方法(JDK)
2012-03-30 16:35 10933转载自 ---- http://www.iteye.co ... -
java的内存管理
2012-03-29 16:59 1601转载自 ---- http://yangzhiyong77 ... -
Java栈与堆
2011-10-10 16:39 865转载自 ---- http://mylir.i ... -
Java内存泄露的理解与解决
2011-10-10 16:38 966转载自 ---- http://henryyang.itey ... -
JVM问题诊断常用命令:jinfo,jmap,jstack
2011-08-18 11:19 1557转载自 ---- http://singleant.iteye ... -
Java HotSpot 性能引擎架构
2011-08-17 17:04 1037转载自 ---- http://lifethink ... -
Java线程安全兼谈DCL
2011-08-17 17:02 1542转载自 ---- http://www.iteye.com/t ... -
CMS gc实践总结(转载)
2011-08-10 15:09 1069首先感谢阿宝 同学的帮助,我才对这个gc算法的调整有 ... -
GC机制小结
2011-08-10 14:07 719转载自 ---- http://zhangjian ... -
排序算法java版(转载)
2011-08-10 14:06 899转载自 ---- http://yiyickf.iteye.c ... -
Java内存模型(JMM) 资料整理(转载)
2011-08-10 13:35 981转载自 ---- http://blog.csdn.net/o ... -
ClassLoader解析(转载)
2011-08-05 14:35 970转载自 ---- http://shangjava ... -
深入理解java的finalize
2011-08-03 17:01 732转载自 ---- http://zhang-xzhi-x ... -
深入理解java的clone
2011-08-03 17:01 767转载自 ---- http://zhang-xzh ...
相关推荐
在 JMM 中,`happen-before` 规则是定义这些同步特性的重要概念。它是一种偏序关系,确保了在程序中两个操作之间的执行顺序,并且保证了前一个操作的结果对后一个操作可见。以下是一些 `happen-before` 的典型规则:...
"深入浅出了解happens-before原则" 在 Java 内存模型(JMM)中,happens-before 原则是一个核心概念,它规定了多线程环境下的可见性原则。该原则的核心思想是:两个操作之间的 happens-before 关系,并不意味着前一...
"Crashes Happen - Downtime Won't with Data Guard"这个主题强调了即使在系统崩溃的情况下,通过Data Guard也能避免停机时间,确保业务连续性。 Data Guard的核心是通过创建一个或多个备用数据库(standby ...
5. **程序次序规则**:同一个线程内的静态初始化和实例初始化按照代码的顺序happens-before。 在生产者消费者例子中,为了解决内存一致性错误,可以使用synchronized关键字来保证原子性和互斥性,或者使用java.util...
本文探讨了Java中volatile关键词的使用和可见性问题,我们将结合JVM内存模型和happen-before概念,来讨论volatile的使用场景和例子。 一、happen-before happen-before是一个可见性原则,定义了Java语言中操作的...
Happen-Before规则是一组用于保证线程之间执行顺序的规则,主要包括: - 程序顺序原则:在一个线程内部保证语义的串行性。 - volatile规则:volatile变量的写操作先于任何读操作。 - 锁规则:解锁操作必然发生在...
$ 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....
Happen-Before内存模型是JSR-133中定义的一种内存模型,它描述了操作之间的顺序关系。这种内存模型可以用来确保在多线程环境下,操作的执行顺序是正确的。 JSR-133是Java语言中非常重要的规范,它定义了Java语言的...
happen-code Get into Leetcode~ Code List 动态规划 动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式...
例如,线程的启动操作Happen-Before该线程的任何读写操作,`synchronized`块的结束Happen-Before同一锁的下次获取,以及对`volatile`变量的写操作Happen-Before其他线程对这个变量的读操作。 线程安全是指在多线程...
ERP Make it Happen ERP Make it Happen ERP Make it Happen ERP Make it Happen
6JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存 7.简单说说你了解的类加载器,可以打破双亲委派么,怎么打破 8.说说你知道的几种主要的JVM参数 9.怎么打出线程栈信息 10....
Project Make It Happen Extension是一个活动日历,可以帮助我们的团队随时了解最新事件,推送通知和每日激励中心。 通过单击第一个选项卡上包含的链接登录到您的后台。 单击第一个选项卡上的日历将跳至第二个选项卡...
安装$ 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 ( '...
请改为使用 。 让它发生的例子 使其成为示例 入门 首先,您可以简单地克隆此存储库并安装所有必需的依赖项。 先决条件 您需要git来克隆MiHexample存储库。 您可以从获得git。 我们还使用了许多node.js工具来初始化...
- **Happen-Before规则**:确保代码执行的有序性,从而保证了数据的可见性。 - 程序顺序规则:代码按照程序顺序执行。 - 监视器锁定规则:对一个监视器的解锁一定发生在后续对同一监视器加锁之前。 - volatile...
例如,可以使用"Happen-Before"的概念来描述指令重排序的规则。在"Happen-Before"中,如果操作A happens-before 操作B,则操作A的结果对操作B是可见的。 此外,还有一个重要的概念是“as-if-serial”,即:不管怎么...