设计模式之单例模式

来源:互联网 发布:ant java 编辑:程序博客网 时间:2024/06/05 12:03

简介

写这篇文章的目的呢,其实很简单,就是让更多的人明白,更加透彻的理解单例模式,或许大家以为单例模式嘛,大家都会些,简简单单,就那么两种,懒汉式或者说恶汉式,在多了解一点的,懒汉式和恶汉式的结合版,呵呵,貌似好像没有什么卵用,接下来,请看正解。


单例模式-情景分析

常见的单例模式代码(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

原创粉丝点击