单例类的使用在JAVA开发者中很常见,然而单例类也给初级开发者带来了许多挑战。其中,最主要的问题是如何保证单例类在任何情形下都保持单实例?Double chencked 机制是一种确保类在应用生命周期里只会被创建一个实例类的方法,正如其名称,Double checked机制会在同步锁内外检测两次,以确保单例类只会有一个实例被创建。不过要注意的是,Double checked机制在JDK1.5前由于JAVA内存模型问题任然无法保证真正的单例。在本文中,我们将会看到JAVA里如何编写double checked机制下的单例类,为什么JAVA 5之前double checked机制有缺陷以及它是怎样被修复的。另外,从技术面试的角度看,Double checked的理解也很重要,我曾听说过在金融和服务业的面试中,要求被试手写出带有double checked机制的单例类,相信我面对这样的问题除非你很清楚知道自己在做什么,否则会有很棘手的问题。你也可以尝试下我准备好的一系列单例设计模式的问题。

为什么单例类需要Double Checked机制?

有一种普遍的情况是多线程下单例类会违反“单例”机制,如果你让一个新手程序猿写出单例模式,大多数情况下他们写出来的代码类似下图所示:


1
private static Singleton _instance;
2

3
public static Singleton getInstance() {
4
        if (_instance == null) {
5
            _instance = new Singleton();
6
        }
7
        return _instance;
8
}

synchronized关键字,就如我们后面提供的第二个代码版本(2nd)所示,尽管这是一个线程安全并且解决了上面多实例问题的方法,但是这样做并不是非常有效率,原本只有当单例类未创建且被访问的时候启用同步锁,然而你却不得不忍受每次访问getInstance()方法都要进行线程锁的判定,大大降低了程序性能。下面的JAVA代码描述了本文所指的Double checked机制,它仅在代码中最需要的部分加上了同步锁,大家称它为Double Checked是因为代码中在同步锁前和锁后用两次 _instance == null 的检查。

ublic static Singleton getInstanceDC() {
02
        if (_instance == null) {                // Single Checked
03
            synchronized (Singleton.class) {
04
                if (_instance == null) {        // Double checked
05
                    _instance = new Singleton();
06
                }
07
            }
08
        }
09
        return _instance;
10
}


表明上看来,上述的Double Checked方法非常完美,它仅在需要的地方加上了同步锁,然而上面的方法需要在变量_instance声明中加入volatile关键字,否则上述方法仍然存在缺陷。 在JAVA 5及其后续版本中,如果添加了volatile关键词修饰,就可以确保任何线程读变量_instance前,该变量已经得到完全的写操作(初始化),这也是为什么JAVA 5之前的Double Checked机制仍然存在漏洞的原因。现在,有了volatile修饰符,我们可以确信代码正确工作了,但是这样的方式并不是线程安全的单例类创建的最好方法,实际上还有两个更好的方式供选择:1. 使用枚举作为单例类(实例创建过程中提供了内置的线程安全性);2.使用static holder 模式。


02
 * A journey to write double checked locking of Singleton class in Java.
03
 */
04
 
05
class Singleton {
06
 
07
    private volatile static Singleton _instance;
08
 
09
    private Singleton() {
10
        // preventing Singleton object instantiation from outside
11
    }
12
 
13
    /*
14
     * 1st version: creates multiple instance if two thread access
15
     * this method simultaneously
16
     */
17
 
18
    public static Singleton getInstance() {
19
        if (_instance == null) {
20
            _instance = new Singleton();
21
        }
22
        return _instance;
23
    }
24
 
25
    /*
26
     * 2nd version : this definitely thread-safe and only
27
     * creates one instance of Singleton on concurrent environment
28
     * but unnecessarily expensive due to cost of synchronization
29
     * at every call.
30
     */
31
 
32
    public static synchronized Singleton getInstanceTS() {
33
        if (_instance == null) {
34
            _instance = new Singleton();
35
        }
36
        return _instance;
37
    }
38
 
39
    /*
40
     * 3rd version : An implementation of double checked locking of Singleton.
41
     * Intention is to minimize cost of synchronization and  improve performance,
42
     * by only locking critical section of code, the code which creates
43
 instance of Singleton class.
44
     * By the way this is still broken, if we don't make _instance volatile,
45
 as another thread can
46
     * see a half initialized instance of Singleton.
47
     */
48
 
49
    public static Singleton getInstanceDC() {
50
        if (_instance == null) {
51
            synchronized (Singleton.class) {
52
                if (_instance == null) {
53
                    _instance = new Singleton();
54
                }
55
            }
56
        }
57
        return _instance;
58
    }
59
}


JAVA中使用枚举实现线程安全的单例类是存在争议的,相比之下有一些更有效的方法来实现单例模式,因此不建议用户自己使用枚举的方法实现JAVA中的单例类。但是这些不建议使用方法的存在有着历史意义,它们揭示出并发情况下是如何引入一些微妙的错误。正如我前面所说,掌握单例模式在面试中也很重要,在任何JAVA面试之前练习写出Double Check机制的单例类可以纠正这类错误见解。