首页 » java » 正文

老生常谈双检锁(DCL)

最近作为平台面试组的一员,对面试工作颇有感触,平台面试组出了一套笔试题,笔试题的最后一道题就是非常经典的实现一个线程安全的懒加载单例模式,并且需要候选人考虑效率来实现。

大家会认为,这么老套的一道面试题,是不是太简单了?非也非也,经过几个星期的面试,并且与每个候选人针对此题进行了沟通,能够真正的答对此题,把此题的每个考核点都答出来的聊聊无几,另外,我注意到,真正的能够写出一个线程安全的懒加载单例模式,即使不要求使用DCL,简单的在方法上使用synchronized来实现,都少有人能够写出正确的实现,可见我们IT行业的编程人员确实有些浮躁,真心建议需要整体提高基础素质。

那么来看看,候选人都是怎么思考本题的?首先,我们先要看题的需求,首先,我们要实现一个懒加载,所以,有些候选人直接来个初始化时候创建实例,这种写法确实很省事,也很实用,可是题目要求写个懒加载,所以,基本需求都没有达到,还有的候选人没有把构造函数私有化,这样就不是一个单例,因为客户程序可以通过构造函数创建多个实例,另外,还有的人单例写对了,但是没有考虑线程安全,也有把实例引用和提供获得实例引用的方法声明成非静态的,还有忘记写return的,最后,还真有一点都写不出来的。

说到这里,大家可以看出,如此简单的一道经典面试题,真的可以拉开候选人的差距,进而区分候选人的水平,如果真的出现上面所说的答案,那基本是及格线以下的,这一类人至少有一半。那么,答出来什么样答案才算及格呢?

在我的面试过程中,如果一个候选人能够写出一个线程安全的懒加载单例模式,而并没有考虑效率,那么这个人就可以及格。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.robert.jvm.cocurrent.dcllock.perf1;


public class SingletonSafe {
    private static SingletonSafe singleton;

    private SingletonSafe() {}

    public static synchronized SingletonSafe getSingleton() {
        if (singleton == null) singleton = new SingletonSafe();

        return singleton;
    }
}

能够写出这个水平的代码的候选人大约有一半吧,因此也就是有一半的人可以及格,那么这么写的代码究竟还会出什么问题呢? 没错,就是题目中所说的效率问题,这种写法果断的把synchronized关键字放在了getSingleton()方法上,这样每个调用getSingleton()方法的线程都会经过synchronized这个锁,也就是在这个方法内部各个线程是串行执行的,这极大的降低了程序的并发效率,如果在一个在线的高并发系统,这个锁一定会成为你的服务的瓶颈。

文章开头的时候,我就说有的候选人会考虑使用初始化时创建实例对象。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.robert.jvm.cocurrent.dcllock.perf1;


public class SingletonEarlyInit {
    private static SingletonEarlyInit singleton = new SingletonEarlyInit();

    private SingletonEarlyInit() {}

    public static SingletonEarlyInit getSingleton() {
        return singleton;
    }
}

类在初始化的时候是单线程的,没有线程竞争,因此在没有任何并发保护的前提下,这种写法仍然是线程安全的。尽管这种写法并没有符合题目要求的懒加载,但是这种写法的效率要高于前者,下面我们作个测试,对比两者的效率。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.robert.jvm.cocurrent.dcllock.perf1;

public class SingletonTest {
    public static long test(int threadCounter, Runnable r) throws InterruptedException {
        Thread[] ts = new Thread[threadCounter];

        for (int i = 0; i < threadCounter; i++) {
            ts[i] = new Thread(r);
        }

        long start = System.currentTimeMillis();

        for (int i = 0; i < threadCounter; i++) {
            ts[i].start();
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].join();
        }

        return System.currentTimeMillis() - start;
    }


    public static void main(String[] args) throws Exception {
        final int threadCounter = 10;
        final int testTimes = 10;
        final int iterTimes = 1000000;

        // Skip the first init method so that it won't cause the deviation of the test result
        SingletonSafe.getSingleton();
        SingletonEarlyInit.getSingleton();

        for (int i = 0; i < testTimes; i++) {
            long safeTime = test(threadCounter, new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < iterTimes; i++) {
                        SingletonSafe.getSingleton();
                    }
                }
            });

            long earlyInitTime = test(threadCounter, new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < iterTimes; i++) {
                        SingletonEarlyInit.getSingleton();
                    }
                }
            });

            System.out.println("Synchronized->" + safeTime + ", EarlyInit->" + earlyInitTime);

        }
    }
}

上面的测试程序,分别使用10个线程来调用安全的懒加载实现和初始化时创建对象的实现程序。

结果如下:

Synchronized->377, EarlyInit->6
Synchronized->290, EarlyInit->0
Synchronized->336, EarlyInit->0
Synchronized->348, EarlyInit->1
Synchronized->265, EarlyInit->1
Synchronized->349, EarlyInit->1
Synchronized->343, EarlyInit->1
Synchronized->300, EarlyInit->1
Synchronized->283, EarlyInit->1
Synchronized->299, EarlyInit->12

可见,安全的懒加载实现,既在方法上使用 synchronized关键字的程序,每次需要300+毫秒的时间,而初始化时创建对象的实现则仅仅需要1+毫秒的时间,可见,性能相差之大,难以想象,如果是一个高并发线上服务,这对整体效率影响是难以想象的。

在笔试环节,也就是候选人参加面试之前做的笔试题答案中,绝大多数的人最好也就写出一个安全的懒加载实现,鲜有能够进一步的考虑到synchronized的效率问题,笔者曾经在阅读JVM相关书籍的时候,得知JVM内部已经对synchronized关键字做了很多优化,包括锁的粗化和细化,轻量级锁,偏向锁等,但是在有线程竞争的境况下,有syncrhonized锁,比没有要慢得多。 上面的测试,就证明了这一点。我的另外一篇帖子Tomcat/Java服务器的日志总结 讲述了synchronized锁对log4j日志框架性能的影响。

好,笔试环节过了,现在到了面试环节,如果候选人的答案通过在方法上增加synchronized关键字写了一个安全的懒加载单例模式,我一般会提示候选人,重新思考下,是不是synchronized锁放在整个方法上效率太低了,考虑到高并发提高效率的目的,能不能进行一些优化?这个时候有一半的候选人故作思索,然后,告诉我这个很为难,没有什么想法,这种现象也很正常,因为在面试的那种环境,少有人能仔细的去思考一个技术问题,很多人选择放弃,一般我会很善良的提醒候选人,效率低的原因是因为把synchronized关键字加在了整个方法了,看看能不能缩小下范围呢?于是,有的候选人有答案了。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.robert.jvm.cocurrent.dcllock.wrongdcl;

import java.util.concurrent.atomic.AtomicLong;

public class Singleton {
    private static Singleton singleton;

    private static AtomicLong counter = new AtomicLong(0);

    private Singleton() {
        counter.incrementAndGet();
    }

    public static Singleton getSingleton() {
        if (singleton == null) { // 判断条件
            synchronized (Singleton.class) {
                /* if (singleton == null) */singleton = new Singleton();
            }
        }
        return singleton;
    }

    public static long getCoutner() {
        return counter.get();
    }


    public static void reset() {
        singleton = null;
        counter.set(0);
    }
}

上面的实现中,增加了一个计数器和一个reset()方法,这都是为了测试方便。

另外,候选人会把synchronized关键字下移,移动到条件判断下面和创建对象实例上面,这类候选人有一定的学习能力,会顺着你的提示来考虑问题,思维有一定的柔软性,一定容易和人交流,所以,对于这类候选人,我在心里都默默的给打上了面试通过的印象。但是,这并不是最好的候选人,因为这个程序有错误,仔细思考以下,如果线程A经过了程序中的判断条件,然后CPU被让出来并且A线程挂起了,同时线程B也经过了判断条件,然后CPU被让出来并且B线程挂起了,这个时候线程A和B顺序通过同步块,就创建了2个实例对象,也就是破坏了单例模式。从下面的测试代码中可以验证上面这个结论。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.robert.jvm.cocurrent.dcllock.wrongdcl;

public class SingletonTest {
    public static void test(int threadCounter) throws InterruptedException {
        Thread[] ts = new Thread[threadCounter];

        for (int i = 0; i < threadCounter; i++) {
            ts[i] = new Thread() {
                @Override
                public void run() {
                    Singleton t = Singleton.getSingleton();
                }
            };
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].start();
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].join();
        }

        if (Singleton.getCoutner() != 1) throw new RuntimeException("Multiple instances (" + Singleton.getCoutner() + ") are created!");
    }

    public static void main(String[] args) throws Exception {
        final int threadCounter = 10;
        final int testTimes = 100;

        for (int i = 0; i < testTimes; i++) {
            test(threadCounter);
            Singleton.reset();
        }
    }
}

测试程序输出如下:

Exception in thread "main" java.lang.RuntimeException: Multiple instances (2) are created!
	at com.robert.jvm.cocurrent.dcllock.wrongdcl.SingletonTest.test(SingletonTest.java:24)
	at com.robert.jvm.cocurrent.dcllock.wrongdcl.SingletonTest.main(SingletonTest.java:32)

可见,在这个测试中,有2个实例被创建了,因此破坏了单例模式。

问题考虑到这里基本就很清晰了,兵来将挡,水来土掩,碰上啥问题,就解决啥问题,既然这种实现存在的问题是多个线程可能进入了同步块,跃跃欲试的去创建对象,那解决办法是不是很简单?啥?不简单?好吧,那我告诉你,在同步块内,再用判断条件过滤一遍,是不是就解决问题了呢?实际上上面代码已经注释了这一块。 :)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.robert.jvm.cocurrent.dcllock.perf2;

import com.robert.jvm.cocurrent.dcllock.memvisible1.Singleton;


public class SingletonDcl {
    private static SingletonDcl singleton = new SingletonDcl();

    private SingletonDcl() {}

    public static SingletonDcl getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) singleton = new SingletonDcl();
            }
        }
        return singleton;

    }
}

于是,双检锁(Double Check Lock)或者DCL就诞生了,本人为了叫起来顺口,有时候会叫双锁,这也就是本题的标准答案。下面测试下安全的懒加载实现和DCL实现的效率差异。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.robert.jvm.cocurrent.dcllock.perf2;

public class SingletonTest {
    public static long test(int threadCounter, Runnable r) throws InterruptedException {
        Thread[] ts = new Thread[threadCounter];

        for (int i = 0; i < threadCounter; i++) {
            ts[i] = new Thread(r);
        }

        long start = System.currentTimeMillis();

        for (int i = 0; i < threadCounter; i++) {
            ts[i].start();
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].join();
        }

        return System.currentTimeMillis() - start;
    }


    public static void main(String[] args) throws Exception {
        final int threadCounter = 10;
        final int testTimes = 10;
        final int iterTimes = 1000000;

        // Skip the first init method so that it won't cause the deviation of the test result
        SingletonSafe.getSingleton();
        SingletonDcl.getSingleton();

        for (int i = 0; i < testTimes; i++) {
            long safeTime = test(threadCounter, new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < iterTimes; i++) {
                        SingletonSafe.getSingleton();
                    }
                }
            });

            long earlyInitTime = test(threadCounter, new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < iterTimes; i++) {
                        SingletonDcl.getSingleton();
                    }
                }
            });

            System.out.println("Synchronized->" + safeTime + ", EarlyInit->" + earlyInitTime);

        }
    }
}

程序输出如下:

Synchronized->391, DCL->7
Synchronized->342, DCL->1
Synchronized->343, DCL->1
Synchronized->316, DCL->0
Synchronized->313, DCL->1
Synchronized->373, DCL->1
Synchronized->321, DCL->1
Synchronized->377, DCL->0
Synchronized->360, DCL->1
Synchronized->320, DCL->12

可见,双检锁的效率和无锁的初始化创建对象的实现版本基本差不多,远远高于安全的懒加载实现。

本来文章写到这里,基本就可以结束了,如果一个候选人写出来双检锁,那必须通过啊,如果这个候选人之前准备过,那赞这个候选人的勤奋好学,如果这个候选人之前没准备过,在我的提示下一步一步的完成一个双检锁,那就赞这个候选人的聪明才智,无论是哪种,都值得赞,都是个好程序员。可以说,懂得双检锁的程序员是好程序员。:)

那么,双检锁是不是真的就没有问题了呢?

首先,我们得先了解下JVM是如何加载一个类的?也就是JVM加载一个类会经历多少个步骤?

先来个导航吧:load -> link(verify,prepare,resolve) -> initialize

JVM加载一个类大体分为三个步骤:

1)加载阶段(load):就是在硬盘上寻找java文件对应的class文件,并将class文件中的二进制数据加载到内存中,将其放在运行期数据区的方法区中去,然后在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构;

2)连接阶段(link):这个阶段分为三个步骤,步骤一:验证(verify),当然是验证这个class文件里面的二进制数据是否符合java规范;步骤二:准备(prepare),为该类的静态变量分配内存空间,并将变量赋一个默认值,比如int的默认值为0;步骤三:解析(resolve),这个阶段就不好解释了,将符号引用转化为直接引用,涉及到指针;

3)初始化阶段(initialize):当我们主动调用该类的时候,将该类的变量赋于正确的值(这里不要和第二阶段的准备混淆了),举个例子说明下两个区别,比如一个类里有private static int i = 5; 这个静态变量在”准备”阶段会被分配一个内存空间并且被赋予一个默认值0,当道到初始化阶段的时候会将这个变量赋予正确的值即5,了解了吧!

知道了JVM加载代码的步骤后,我们进一步拆解双检锁模式的代码。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.robert.jvm.cocurrent.dcllock.perf2;

import com.robert.jvm.cocurrent.dcllock.memvisible1.Singleton;


public class SingletonDcl {
    private static SingletonDcl singleton = new SingletonDcl();

    private SingletonDcl() {}

    public static SingletonDcl getSingleton() {
        if (singleton == null) { //判断条件1
            synchronized (Singleton.class) {
                if (singleton == null) { //判断条件2
                    // Step1. 为SingletonDcl实例分配内存
                    // Step2. 将内存引用赋值给singleton变量
                    // Step3. 初始化对象
                }
            }
        }
        return singleton;
    }
}

想象这样的一个场景,线程A经过了判断条件1和判断条件2,并且走过了Step1和Step2,也就是当前对象已经分配了内存,但是没有进行初始化,并且静态引用变量singleton指向了没有初始化的对象实例内存,这时如果线程B正好走到判断条件1,发现了singleton引用并不为空,所以,线程A就拿到了一个未进行初始化的对象,这个时候访问此对象的变量,这些变量可能也没有初始化完成,就产生了问题。

那么这个问题肿麽办?肿麽办?如果百度一下,会有很多路人博客推荐大家使用volatile防止指令重排,可是我一直不理解,产生问题的根源原因,是在一个关键段的中间引用被同步到了主内存才会出现这个问题,volatile恰恰是保证引用能够实时的同步到主存,怎么就能解决这个问题呢?

政治课上唯一教会我的就是,实践是检验真理的唯一标准,上面程序的Step1, Step2和Step3已经被包含在一个synchronized关键块,synchronized关键字除了互赤性,还有与volatile相似的内存可见性,也就是JVM的实现会保证其内部的动作原子性的从线程的工作拷贝更新到主存,因此不会出现上面的问题,好了,为了验证我的分析,我做了一个压测,通过100次,每次10线程的测试发现,上面的说法是total nonsense, 啥?不知道total nonsense啥意思啊?先学学东北话就懂了。:)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.robert.jvm.cocurrent.dcllock.memvisible1;

import java.util.concurrent.atomic.AtomicLong;

public class Singleton {
    private static Singleton singleton;

    private static AtomicLong counter = new AtomicLong(0);

    private int data = 0;

    private Singleton() {
        counter.incrementAndGet();
        data = 10000;
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) singleton = new Singleton();
            }
        }
        return singleton;
    }

    public int getData() {
        return data;
    }

    public static long getCoutner() {
        return counter.get();
    }


    public static void reset() {
        singleton = null;
        counter.set(0);
    }
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.robert.jvm.cocurrent.dcllock.memvisible1;

public class SingletonTest {
    public static void test(int threadCounter) throws InterruptedException {
        Thread[] ts = new Thread[threadCounter];

        for (int i = 0; i < threadCounter; i++) {
            ts[i] = new Thread() {
                @Override
                public void run() {
                    Singleton t = Singleton.getSingleton();

                    if (t.getData() != 10000)
                        throw new RuntimeException("Expose the singleton whitouting initializing its primitive data field!");
                }
            };
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].start();
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].join();
        }

        if (Singleton.getCoutner() != 1) throw new RuntimeException("Multiple instances (" + Singleton.getCoutner() + ") are created!");
    }

    public static void main(String[] args) throws Exception {
        final int threadCounter = 10;
        final int testTimes = 100;

        for (int i = 0; i < testTimes; i++) {
            test(threadCounter);
            Singleton.reset();
        }
    }
}

测试过程中,如果发现某个线程拿到的引用singleton的私有数据类型data没有初始化,则抛出异常,可是事实证明,这种情况并没有发生。

查阅了资料发现,DCL的安全性最初被公开的时候在2001年JavaWorld。其实那个例子在那个时候已经是错误的。影响了后面几代人都跟着错误走,有时候一些非常简单的问题因为放在权威的地方就没有人敢去责疑。

最初的问题提出者在这儿: http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html

例子是:

1
2
3
4
5
6
7
8
9
10
11
12
class SomeClass {
  private Resource resource = null;
  public Resource getResource() {
    if (resource == null) {
      synchronized {
        if (resource == null)
          resource = new Resource();
      }
    }
    return resource;
  }
}

我不知道提这个问题的人是否是个老古董。在2001年,JVM1.2已经发布好久了。JMM(Java Memory Model)已经已经发布了新的规范,竟然在这个时候提出这样的问题。而且这个已经不存在的问题在此后的十几年中一直误导着很多人,甚至是一些技术牛人,CTO,博士等。

这个问题的提出者是这样说的.当一个线程运行到resource = new Resource();时,因为new一个对象需要分配空间,初始化字段,调用构造方法。当为resource分配好空间后,外面的其它线程就可以看到不为null的resource,而这时有可能还没有初始化字段和调用构造方法就被其它线程引用了。确实,在JAVA2(以jdk1.2开始)以前对于实例字段是直接在主储区读写的。所以当一个线程对resource进行分配空间,初始化和调用构造方法时,可能在其它线程中分配空间动作可见了,而初始化和调用构造方法还没有完成.

但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有向主存储区复制赋值时,其它线程绝对不可能见到这个过程。而这个字段复制到主存区的过程,更不会有分配空间后没有初始化或没有调用构造方法的可能。并且这些动作是包含在synchronized关键字代码块里面的,在JAVA中,一切都是按引用的值复制的。向主存储区同步其实就是把线程工作存储区的这个已经构造好的对象有压缩堆地址值COPY给主存储区的那个变量。这个过程对于其它线程,要么是resource为null,要么是完整的对象.绝对不会把一个已经分配空间却没有构造好的对象让其它线程可见.

到这里,可以说上面的那个质疑完全是胡说,事实证明,JLive, OFBiz这些开源项目中都在大量应用DCL,而并没有出现并发的安全问题,所以,在当今的JVM上,你可以放心大胆的使用DCL。

野火烧不尽,春风吹又生,问题到这还真没有结束,有人提出,DCL还是不安全的,为什么不安全呢?问题转到了同一对象不同引用字段上面, 如果单例对象里面包含一个引用字段,引用字段需要进行初始化,这有别于私有类型的初始化,也就是初始化是在构造函数中进行的,那么就有可能发生singleton字段和它内部的引用字段的初始化不是在一个关键段内完成的,因为这里线程的内存拷贝里有2个引用变量, 一个是singleton一个是它的子字段,那么在往主存同步的时候会有个先后顺序,如果singleton先被同步, 它的子字段后被同步,那么就有个时间窗口内,在这个时间窗口内singleton被其他线程看见,而它的子字段却没有被初始化,就出现了问题,是不是听着很有道理。

还是那句话,实践是检验真理的唯一标准。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.robert.jvm.cocurrent.dcllock.memvisible2;

import java.util.concurrent.atomic.AtomicLong;

public class Singleton {
    private static Singleton singleton;

    private static AtomicLong counter = new AtomicLong(0);

    private StringBuffer data;

    private Singleton() {
        counter.incrementAndGet();
        data = new StringBuffer("inited");
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) singleton = new Singleton();
            }
        }
        return singleton;
    }

    public StringBuffer getData() {
        return data;
    }

    public static long getCoutner() {
        return counter.get();
    }


    public static void reset() {
        singleton = null;
        counter.set(0);
    }

}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.robert.jvm.cocurrent.dcllock.memvisible2;

public class SingletonTest {
    public static void test(int threadCounter) throws InterruptedException {
        Thread[] ts = new Thread[threadCounter];
        for (int i = 0; i < threadCounter; i++) {
            ts[i] = new Thread() {
                @Override
                public void run() {
                    Singleton t = Singleton.getSingleton();

                    if (t.getData() == null || !"inited".equals(t.getData().toString()))
                        throw new RuntimeException("Expose the singleton whitouting finishing initializing its object reference field!");
                }
            };
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].start();
        }

        for (int i = 0; i < threadCounter; i++) {
            ts[i].join();
        }

        if (Singleton.getCoutner() != 1) throw new RuntimeException("Multiple instances (" + Singleton.getCoutner() + ") are created!");
    }

    public static void main(String[] args) throws InterruptedException {
        final int threadCounter = 10;
        final int testTimes = 100;

        for (int i = 0; i < testTimes; i++) {
            test(threadCounter);
            Singleton.reset();
        }
    }
}

与我的期望一致,程序并没有输出任何异常,也就是StringBuffer类型的子变量并没有被延迟初始化,这又一次证明了DCL锁是安全的。

从这篇对DCL的研究文章,可以得到如下几个结论:

1. 凡事都不是看起来那么简单。
2. 不要人云亦云,关键时刻需要实践来验证你的想法。
3. 做事细节很重要,很多高大上的技术说起来容易,用起来未必那么好用。
4. 以后谈到DCL的时候,一定要认定它是安全的,如果说不安全,除非你用JDK1.1,可是你用吗?不用,我帮你回答。

参考

http://blog.csdn.net/maritimesun/article/details/7831065
http://www.cnblogs.com/redcreen/archive/2011/03/29/1998802.html

代码

dcllock

本文共 1 个回复

Comments are closed.