简介
写这篇文章的目的呢,其实很简单,就是让更多的人明白,更加透彻的理解单例模式,或许大家以为单例模式嘛,大家都会些,简简单单,就那么两种,懒汉式或者说恶汉式,在多了解一点的,懒汉式和恶汉式的结合版,呵呵,貌似好像没有什么卵用,接下来,请看正解。
单例模式-情景分析
常见的单例模式代码(1/3)
这个呢,其实大家都知道,这是最为常见的单例模式,也就是单例中的懒汉式,就是在你需要使用对象的时候,在去new 一个出来,那么这样下有什么特点:
特点:延迟加载,需要的时候才使用。
缺点:单线程使用是没问题的,但是我们的web开发都是需要并发的,所以这个代码适用场景较小
常见的单例模式代码(2/3)
这种,也就是单例模式中的恶汉式,就是在一开始,我就给new 出对象,就像一个饿了的人,太饿了,先拿着东西吃,那么这个有什么特点了
优点:线程安全
缺点:不是延迟加载的,当构造这样的单例非常重量级,但是这个单例可能很久也用不到一次的时候比较受伤。(这样的场景比较少)
常见的单例模式代码(3/3)
那么这种情况下的单例模式,你们是不是就觉得少见了,对于大部分人来说,知道前面两种是必然,知道以下的那么就可能没多少人了吧,我来解释下这个代码
使用内部类方式(static holder),既支持了延迟加载,也是线程安全的,较完美的解决方案。
这种呐,一般情况下,绝大多数人都是这样去写的,这样的解决方案的确算是接近完美了。
是不是大家以为,单例模式到这里基本是就接近于结束了,over了,那你就大错特错了,加下来带你看看其他的东西
思考点
第一种单例模式
回过头看来第一种单例模式的代码,要让他支持多线程环境,有其他改动方法么?
上面我们知道这种代码的特点了,那么针对这种情况,我们有上面好的方法解决了呐
常见的解决方法
我们在这个地方增加synchronized关键字,这样是线程安全的,但是这样做有什么缺点呢?
因为在方法上使用了synchronized关键词,每次获取单例都要同步,而同步的成本较高,所有讲同步的书籍都告诉我们,要缩小synchronized使用的范围
那么我们如何解决?
lowB一些的会这样改
这样又会出现什么问题?
前述代码存在多线程bug,有可能会创建多个对象
那要怎么改进呢?
牛逼一些的会这样写:
这样写的原因是什么:
1.这样的代码有个术语,叫:双重检查锁定(DCL),前述代码貌似解决了多线程bug,把synchronized限定在最小的范围,拥有不错的性能,而且是懒加载的,好像无可挑剔,实际上我也看到过不少人是这么写的。
2.表面看起来确实是这样,然而不幸的是:这个代码仍然有问题
问题在那?
一、使用DCL能确保只生成单例,他的问题在于可能让其他线程看到这个单例的未构建完全的样子。
二、DCL背后的理论是完美的,他失败的原因不是jvm的bug,而是java的内存模型导致的,关键原因在于:new操作不是原子的,而java内存模型允许指令重排。
一:大家认为的new对象的过程
比如instance = new Instance();这样的代码,编译器可以翻译为三步:
1).mem= allocate();//分配内存
2).callConstructor(mem);//调用构造函数
3). Instance=mem;//把内存指针赋值给instance。
如果是这样的方式,DCL可以正常工作。
二:指令重排
但是编译器也可能按这样的顺序执行:
1).mem= allocate();//分配内存
2).Instance=mem;//把内存指针赋值给instance,注意此时instance已经是非null了。
3).callConstructor(instance);//调用构造函数
这种情况下,DCL会失效。
三:了解了指令重排,有人会这样改
Will it work?
三:甚至这样改(事情将变的复杂)
fuck了,估计到这里,我们就会想,真是日了狗了,有必要嘛,一个单例搞这么复杂,搞飞机啊,哎。。。。。。
前面两种代码同样存在指令重排的问题。
DCL的另一个问题
上述DCL代码还存在另一个问题,并发编程的经验告诉我们:必须对同一个变量的所有读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的。就是说一个线程调用单例方法生成单例后,另一个线程刚进入方法时仍然可能看到是null的,直到进入同步块才能看到那个对象,存在浪费。
顺着这个思路,难道就没有办法了吗?
在JDK5之前,答案:有更曲折的解决方案,使用ThreadLocal代替外层的null检查,当然效率较低。(感兴趣的可以自行google,这里不讨论)
庆幸的是,java一直在改进其内存模型
在JDK5及其之后,已经有办法稍微改下代码,使DCL可用。
how?
答案是在类变量上加入volatile关键词
but why ?
在JDK5之前,使用volatile往往不能得到正确的结果,JDK5对volatile的语义做了重大改变。
其中有两个比较关键:
1.禁止指令重排序
2.内存可见性
规则1可以确保外层null检查要么看到的是null,要么看到的是一个构造完整的对象,规则2可以解决之前浪费的问题。
综上所述,用DCL来实现单例并不是一种很好的方法,因为过于复杂,容易出错。更好的方式是使用之前说到的static holder方式,或者枚举单例。
枚举单例
业界非常推崇用此法实现单例,因为它的简单,以及如下好处:
好处:
1.线程安全;
2.不会因为序列化而产生新的实例(因为它自己实现了readResolve方法);
3.防止反射攻击。(因为enum实际上是abstract的)
http://segmentfault.com/q/1010000000646806
相关资料
关于volatile关键字:
http://www.cnblogs.com/dolphin0520/p/3920373.html
关于DCL:
http://www.iteye.com/topic/260515
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html