分享

怎样实现线程安全的延迟初始化单例模式 ??

 CevenCheng 2011-08-10

在实际开发过程中,常见使用Double Check Locking(DCL)实现延迟初始化的单例模式。在Java中,虽然这早已被证实是一种有害的编程习惯,但这并不能阻止它在程序员之间的传播。DCL存在线程安全隐患,不少Java编程书上有关于这个问题的介绍,网上也很多讨论。不过通常情况(并发量不大、安全性要求不高)下,它能够工作得很好,这大概就是它得以流传的原因吧。

DCL是一种反模式,典型的DCL如下:

01public class Singleton {
02    private static Singleton instance = null;
03 
04    private Singleton() {
05        initialize();
06    }
07 
08    public static Singleton getInstance() {
09        if (instance == null) {
10            sychronized (Singleton.class) {
11                if (instance == null) {
12                    instance = new Singleton();
13                }
14            }
15        }
16        return instance;
17    }
18}

按照网上人们对DCL的分析,线程安全问题在于instance = new Singleton();这行代码:第一个线程执行到此处时,可能是先分配对象的内存空间,在尚未初始化对象之前,已将这块空间的地址赋值给instance变量,之所以这样,是因为编译器会执行指令重排(statement reordering)的优化,使得实际执行的指令顺序并非按照语句的自然顺序,即乱序(out of order)执行。进而导致第二个线程在外层if条件处跳过,直接返回instance引用,使得对象处在不完整的状态,这被称为Unsafe Publication(见Java Concurrency in Pratice)。

曾经遭遇过一次由延迟初始化带来的问题,对DCL的危害才有了切身的体会。当时的代码大体如下:

01public class MemcachedService {
02    private static MemcachedService instance = null;
03 
04    private MemcachedService() {
05        initialize();
06    }
07 
08    public static MemcachedService getInstance() {
09        if (instance == null) {
10            sychronized (MemcachedService.class) {
11                instance = new MemcachedService();
12            }
13        }
14        return instance;
15    }
16 
17    public Object getValue(String key) {
18        //retrieve and return cache obj
19    }
20}

为简明起见,省略了部分代码。
实际上,这段代码并非DCL,它比DCL的安全性更弱。它本来是希望延迟实例化对象,并通过sychronized块限制对象只实例化一次。而由于if判断和sychronized块之间不是原子操作,实例化可能会执行多次。再者,仍然是instance = new MemcachedService();这行代码,对MemcachedService实例的创建和对instance变量的写入操作可能会被重排,造成Unsafe Publication。

这段代码是用在一个web应用程序中,用来从cache中获取事先存储的数据。在测试环境中一切良好,一部署到线上环境,即发现getValue处无法获取cache值。开始怀疑是环境问题,后来在线上环境同一台机器另部署了一套相同的程序,一切正常。为这个问题几乎折腾了一整天,当最后定位到延迟初始化问题时,原因便显而易见:线程安全缺陷通常在高并发的情况下问题才会显露,测试环境由内部人员使用,访问量很小,也没有进行压力测试,所以问题没有呈现;而线上环境并发访问量很高,程序一上线,问题立马显露无疑。

既然DCL存在线程安全隐患,且一旦出问题又难以调试,所以人们建议永远不要在Java程序中使用DCL。实际上,在整个应用范围内的单例(特别是在服务端程序中),往往没必要采用延迟初始化。延迟初始化主要是为了让一些昂贵的操作,在真正需要时才被执行。对于为大量持续不断的客户请求提供服务的程序来说,对单例的访问总是急切的,所以延迟初始化没有什么实际意义。

上面的代码改写成这样(急切初始化)既简洁又安全,同时消除了同步的开销:

01public class MemcachedService {
02    private static final MemcachedService instance = newMemcachedService();
03 
04    private MemcachedService() {
05        try {
06            initialize();
07        catch (Exception e) {
08            //handle exception
09        }
10    }
11 
12    public static MemcachedService getInstance() {
13        return instance;
14    }
15 
16    public Object getValue(String key) {
17        //retrieve and return cache obj
18    }
19}

update @2011-07-12
感谢scala指正,上述代码有安全问题,通过反射就能创建多个实例。


===================  Effect Java 推荐延迟初始化单例模式 ================

针对 liuchangit 称自己为 java geek,说明还是个会不断思考钻研的人 ,扣点小东西 呵呵。

(1)
既能延迟加载 又能规避多线程问题,以下只是个粗略的版本,少了private构造 还有很多安全问题
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
(2)MemcachedService 这个类是有安全问题的,通过反射就能创建多个实例,如果有人想搞你的话,当然 这种概率很小,所以 针对geek 才会说2句 现在能说2句的真的太少了。。。。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多