`
iamzhongyong
  • 浏览: 796698 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

【转】最简单也最复杂的设计模式

 
阅读更多

 

一直觉得单例模式自己掌握的挺好,但是看完这篇文章后汗颜了。。。

 

前面说提到的五种创建模式,主要解决的问题是如何创建对象,获得产品。而单例模式最要关心的则是对象创建的次数以及何时被创建。

 

Singleton模式可以是很简单的,它的全部只需要一个类就可以完成(看看这章可怜的UML图)。但是如果在“对象创建的次数以及何时被创建”这两点上较真起来,Singleton模式可以相当的复杂,比头五种模式加起来还复杂,譬如涉及到DCL双锁检测(double checked locking)的讨论、涉及到多个类加载器(ClassLoader)协同时、涉及到跨JVM(集群、远程EJB等)时、涉及到单例对象被销毁后重建等。对于复杂的情况,本章中会涉及到其中一些[1]

 

目的:

希望对象只创建一个实例,并且提供一个全局的访问点。

 

场景:

Kerrigan对于Zerg来说是个至关重要的灵魂人物,无数的DroneZerglingHydralisk……可以被创造、被牺牲,但是Kerrigan得存在关系到Zerg在这局游戏中的生存,而且Kerrigan是不允许被多次创造的,必须有且只有一个虫族刀锋女王的实例存在,这不是游戏规则,但这是个政治问题。

 

分析:

 

如前面一样,我们还是尝试使用代码来描述访问Kerrigan的过程,看看下面的UML图,简单得我都不怎么好意思放上来占版面。

 

6.1 单例模式的UML

 

结构是简单的,只是我们还有一些小小的要求如下:

 

1.最基本要求:每次从getInstance()都能返回一个且唯一的一个Kerrigan对象。

2.稍微高一点的要求:Kerrigan很忙,很多人找,所以希望这个方法能适应多线程并发访问。

3.再提高一点的要求:Zerg是讲究公务员效率的社会,希望找Kerrigan的方法性能尽可能高。

4.最后一点要求是Kerrigan自己提出的:体谅到Kerrigan太累,希望多些睡觉时间,因此Kerrigan希望实现懒加载(Lazy Load),在需要的时候才被构造。

5.原本打算说还提要处理多ClassLoader、多JVM等情况,不过还是不要把情况考虑的太复杂了,暂且先放过作者吧(-_-#)。

 

我们第一次写的单例模式是下面这个样子的:

 

Java代码 
  1. /** 
  2.  * 实现单例访问Kerrigan的第一次尝试 
  3.  */  
  4. public class SingletonKerriganA {  
  5.    
  6.     /** 
  7.      * 单例对象实例 
  8.      */  
  9.     private static SingletonKerriganA instance = null;  
  10.    
  11.     public static SingletonKerriganA getInstance() {  
  12.         if (instance == null) {                              //line A  
  13.             instance = new SingletonKerriganA();          //line B  
  14.         }  
  15.         return instance;  
  16.     }  
  17. }  
 

 

 

这个写法我们把四点需求从上往下检测,发现第二点的时候就出了问题,假设这样的场景:两个线程并发调用SingletonKerriganA.getInstance(),假设线程一先判断完instance是否为null,既代码中的line A进入到line B的位置。刚刚判断完毕后,JVMCPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然是空的,因此线程二执行了new SignletonKerriganA()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new SignletonKerriganA()操作,好了,问题来了,两个Kerrigan谁是李逵谁是李鬼?

 

紧接着,我们做单例模式的第二次尝试:

 

Java代码 
  1. /** 
  2.  * 实现单例访问Kerrigan的第二次尝试 
  3.  */  
  4. public class SingletonKerriganB {  
  5.    
  6.     /** 
  7.      * 单例对象实例 
  8.      */  
  9.     private static SingletonKerriganB instance = null;  
  10.    
  11.     public synchronized static SingletonKerriganB getInstance() {  
  12.         if (instance == null) {  
  13.             instance = new SingletonKerriganB();  
  14.         }  
  15.         return instance;  
  16.     }  
  17. }  
 

 

 

 

比起第一段代码仅仅在方法中多了一个synchronized修饰符,现在可以保证不会出线程问题了。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了SingletonKerriganB的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上说很不划算。

 

那继续把代码改成下面的样子:

 

Java代码 
  1. /** 
  2.  * 实现单例访问Kerrigan的第三次尝试 
  3.  */  
  4. public class SingletonKerriganC {  
  5.    
  6.     /** 
  7.      * 单例对象实例 
  8.      */  
  9.     private static SingletonKerriganC instance = null;  
  10.    
  11.     public static SingletonKerriganC getInstance() {  
  12.         synchronized (SingletonKerriganC.class) {  
  13.             if (instance == null) {  
  14.                 instance = new SingletonKerriganC();  
  15.             }  
  16.         }  
  17.         return instance;  
  18.     }  
  19. }  
 

 

 

基本上,把synchronized移动到代码内部是没有什么意义的,每次调用getInstance()还是要进行同步。同步本身没有问题,但是我们只希望在第一次创建Kerrigan实例的时候进行同步,因此我们有了下面的写法——双重锁定检查(DCL)。

 

Java代码 
  1. /** 
  2.  * 实现单例访问Kerrigan的第四次尝试 
  3.  */  
  4. public class SingletonKerriganD {  
  5.    
  6.     /** 
  7.      * 单例对象实例 
  8.      */  
  9.     private static SingletonKerriganD instance = null;  
  10.    
  11.     public static SingletonKerriganD getInstance() {  
  12.         if (instance == null) {  
  13.             synchronized (SingletonKerriganD.class) {  
  14.                 if (instance == null) {  
  15.                     instance = new SingletonKerriganD();  
  16.                 }  
  17.             }  
  18.         }  
  19.         return instance;  
  20.     }  
  21. }  

 

 

看起来这样已经达到了我们的要求,除了第一次创建对象之外,其他的访问在第一个if中就返回了,因此不会走到同步块中。已经完美了吗?

 

我们来看看这个场景:假设线程一执行到instance = new SingletonKerriganD()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:

 

1.Kerrigan的实例分配内存。

2.初始化Kerrigan的构造器

3.instance对象指向分配的内存空间(注意到这步instance就非null了)。

 

但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMMJava Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。

 

DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证23步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本,所以本文中单例模式写法的改进还在继续。

 

代码倒越来越复杂了,现在先来个返璞归真,根据JLSJava Language Specification)中的规定,一个类在一个ClassLoader中只会被初始化一次,这点是JVM本身保证的,那就把初始化实例的事情扔给JVM好了,代码被改成这样:

 

Java代码 
  1. /** 
  2.  * 实现单例访问Kerrigan的第五次尝试 
  3.  */  
  4. public class SingletonKerriganE {  
  5.    
  6.     /** 
  7.      * 单例对象实例 
  8.      */  
  9.     private static SingletonKerriganE instance = new SingletonKerriganE();  
  10.    
  11.     public static SingletonKerriganE getInstance() {  
  12.         return instance;  
  13.     }  
  14. }  
 

 

 

好吧,如果这种写法是完美的话,那前面那么几大段话就是作者在消遣各位读者。这种写法不会出现并发问题,但是它是饿汉式的,在ClassLoader加载类后Kerrigan的实例就会第一时间被创建,饿汉式的创建方式在一些场景中将无法使用:譬如Kerrigan实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

 

再来看看下面这种我觉得能应对较多场景的单例写法:

 

Java代码 
  1. /** 
  2.  * 实现单例访问Kerrigan的第六次尝试 
  3.  */  
  4. public class SingletonKerriganF {  
  5.    
  6.     private static class SingletonHolder {  
  7.         /** 
  8.          * 单例对象实例 
  9.          */  
  10.         static final SingletonKerriganF INSTANCE = new SingletonKerriganF();  
  11.     }  
  12.    
  13.     public static SingletonKerriganF getInstance() {  
  14.         return SingletonHolder.INSTANCE;  
  15.     }  
  16. }  
 

 

 

这种写法仍然使用JVM本身机制保证了线程安全问题;由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。

 

其他单例模式的写法还有很多,如使用本地线程(ThreadLocal)来处理并发以及保证一个线程内一个单例的实现、GoF原始例子中使用注册方式应对单例类需要需要继承时的实现、使用指定类加载器去应对多ClassLoader环境下的实现等等。我们做开发设计工作的时,应当既要考虑到需求可能出现的扩展与变化,也应当避免“幻影需求”导致无谓的提升设计、实现复杂度,最终反而带来工期、性能和稳定性的损失。设计不足与设计过度都是危害,所以说没有最好的单例模式,只有最合适的单例模式。

 

到这里为止,单例模式本身就先告一段落了,最后在介绍从其他途径屏蔽构造单例对象的方法:

 

1.直接new单例对象

2.通过反射构造单例对象

3.通过序列化构造单例对象。

 

对于第一种情况,一般我们会加入一个private或者protected的构造函数,这样系统就不会自动添加那个public的构造函数了,因此只能调用里面的static方法,无法通过new创建对象。

 

对于第二种情况,反射时可以使用setAccessible方法来突破private的限制,我们需要做到第一点工作的同时,还需要在在ReflectPermission("suppressAccessChecks") 权限下使用安全管理器(SecurityManager)的checkPermission方法来限制这种突破。一般来说,不会真的去做这些事情,都是通过应用服务器进行后台配置实现。

 

对于第三种情况,如果单例对象有必要实现Serializable接口(很少出现),则应当同时实现readResolve()方法来保证反序列化的时候得到原来的对象。

 

基于上述情况,将单例模式增加两个方法:

 

Java代码 
  1. /** 
  2.  * 能应对大多数情况的单例实现 
  3.  */  
  4. public class SingletonKerrigan implements Serializable {  
  5.    
  6.     private static class SingletonHolder {  
  7.         /** 
  8.          * 单例对象实例 
  9.          */  
  10.         static final SingletonKerrigan INSTANCE = new SingletonKerrigan();  
  11.     }  
  12.    
  13.     public static SingletonKerrigan getInstance() {  
  14.         return SingletonHolder.INSTANCE;  
  15.     }  
  16.    
  17.     /** 
  18.      * private的构造函数用于避免外界直接使用new来实例化对象 
  19.      */  
  20.     private SingletonKerrigan() {  
  21.     }  
  22.    
  23.     /** 
  24.      * readResolve方法应对单例对象被序列化时候 
  25.      */  
  26.     private Object readResolve() {  
  27.         return getInstance();  
  28.     }  
  29. }  
 

 

 

 

总结:

本章通过一次次的的尝试,去了解单例模式各种实现方案的优缺点。对双锁锁定检测进行了简单的讨论,相信大家能从各种尝试的演化过程中,明白为何单例模式是最简单而又最复杂的一种构造模式。

 

各种构造模式之间可以互相比较,但是没有优劣好坏之分,只有确定了上下文环境,才能谈应用什么模式。学习设计模式我觉得也没有必要去强背一些代码模版,应当去理解每种模式的出现的原因和解决的问题,当你发现你的设计需要更大灵活性时,设计便会向着合适的模式演化,这时候你就真正的掌握了设计模式。

 



转自:

http://www.iteye.com/topic/575052

 

 

分享到:
评论
1 楼 kelly_zsl 2014-07-04  
使用DCL话,加上volatile修饰对象也保证不了单例模式正确性,如果volatile修饰是primitive就可以保证。个人看法,毕竟volatile 只是控制 cpu不要使用register里面数据,但是对象初始化的过程还是保证部了的

相关推荐

    GoF 的 23 种设计模式

    GoF 的 23 种设计模式的分类,现在对各个模式的功能进行介绍。 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。 原型(Prototype)模式:将一...

    DesignPatternsLibrary:一个用C#实现的全面的设计模式库,涵盖了从最常用的模式到鲜为人知的各种设计模式。 通过适度现实的示例熟悉并学习设计模式

    设计模式库32个设计模式•65个中等逼真的示例什么是设计模式? 在软件工程中,设计模式是解决软件设计中常见问题... 结构性结构设计模式是通过识别实现实体之间关系的简单方法来简化设计的设计模式。 结构模式与类和对

    领域驱动设计与模式实战

    2.4 针对具体应用程序类型的设计模式 2.5 领域模式 2.6 小结 第3章 TDD与重构 3.1 TDD 3.1.1 TDD流程 3.1.2 演示 3.1.3 设计效果 3.1.4 问题 3.1.5 下一个阶段 3.2 模拟和桩 3.2.1 典型单元测试 3.2.2 声明独立性 ...

    基于Java的XML解析与反射设计模式.doc

    基于Java的XML解析与反射设计模式 摘要:随着计算机时代的蓬勃发展,internet的普及给社会以及人民的生活带来了巨 大的影响。与此同时,b/s结构的多系统相互访问技术应时代的发展也如雨后春笋般不断 涌现出来,相应...

    asp.net知识库

    2分法-通用存储过程分页(top max模式)版本(性能相对之前的not in版本极大提高) 分页存储过程:排序反转分页法 优化后的通用分页存储过程 sql语句 一些Select检索高级用法 SQL server 2005中新增的排序函数及应用 ...

    windows环境下32位汇编语言程序设计

    对于Win32汇编也是如此,从最简单的例子开始总是没错的,笔者建议读者跟随本书中从简到繁的例子,努力做到理解并灵活引用这些例子中的各种功能,正如“熟读唐诗三百首,不会写诗也会吟”,最后能够熟练地使用Win32...

    摩托罗拉C++面试题

    不过,我不建议滥用设计模式,以为它有可能使得简单问题复杂化. 7.介绍一下你对设计模式的理解。(这个过程中有很多很细节的问题随机问的) 设计模式概念是由建筑设计师Christopher Alexander提出:"每一个模式描述...

    中型企业网络设计与仿真毕业设计.doc

    经需求分析,得出以下结论: 2.1 硬件需求 (1)对于中小企业,采用基于TCP/IP协议组的以太交换网模式是最适合的。经过几年 的发展,以太交换技术和产品都十分成熟,网络的实现和管理简单,维护量小,并且可 以向未来...

    浅谈数据库设计.doc

    概念结构设计 概念结构设计的目标是产生出一个能反映组织信息需求的概念模型,其特点有简单明 确表示用户业务数据需求、数据之间的联系、数据约束条件等。概念结构的策略有四种 自顶向下、自底向上、逐步扩张、混合...

    ET CAD最新版

    您可以轻松完成省道、衣褶、转省、圆顺和展开等各种复杂工艺设计。 2.高度的智能化 在刀口设计、缝边添加和缝边角处理等方面,ET具有高度的智能化,并全程自动维护。使用ET,以往烦杂的工作将变得轻松简单。 3....

    设计学生学籍管理系统

    它也奠定了“第四代+”(4Glplus)编程语言的基础,它通过设计、建模、开发、配置和管理的紧密集成大大提高了开发人员的生产力, 1.3.3 管理上的可行性  本系统采用powerbuilder8.0 自带的ASA(Adaptive Server ...

    PHOTOSHOP.LAB修色圣典[中文全彩][六分卷][过路人odv1].part1

    在宣布他当选时,美国Photoshop国家专业协会在颁奖词中说:“Dan能够将复杂的概念简单化为用户可以理解的语言,他处理实际问题的主张已经使他成为当今颜色再现领域最有影响的人物。” 内容简介 Photoshop LAB颜色...

    PHOTOSHOP.LAB修色圣典[中文全彩][六分卷][过路人odv1].part4

    在宣布他当选时,美国Photoshop国家专业协会在颁奖词中说:“Dan能够将复杂的概念简单化为用户可以理解的语言,他处理实际问题的主张已经使他成为当今颜色再现领域最有影响的人物。” 内容简介 Photoshop LAB颜色...

    PHOTOSHOP.LAB修色圣典[中文全彩][六分卷][过路人odv1].part2

    在宣布他当选时,美国Photoshop国家专业协会在颁奖词中说:“Dan能够将复杂的概念简单化为用户可以理解的语言,他处理实际问题的主张已经使他成为当今颜色再现领域最有影响的人物。” 内容简介 Photoshop LAB颜色...

    PHOTOSHOP.LAB修色圣典[中文全彩][六分卷][过路人odv1].part5

    在宣布他当选时,美国Photoshop国家专业协会在颁奖词中说:“Dan能够将复杂的概念简单化为用户可以理解的语言,他处理实际问题的主张已经使他成为当今颜色再现领域最有影响的人物。” 内容简介 Photoshop LAB颜色...

    PHOTOSHOP.LAB修色圣典[中文全彩][六分卷][过路人odv1].part6

    在宣布他当选时,美国Photoshop国家专业协会在颁奖词中说:“Dan能够将复杂的概念简单化为用户可以理解的语言,他处理实际问题的主张已经使他成为当今颜色再现领域最有影响的人物。” 内容简介 Photoshop LAB颜色...

    中型企业网络设计与仿真毕业设计(1).doc

    经需求分析,得出以下结论: 2.1 硬件需求 (1)对于中小企业,采用基于TCP/IP协议组的以太交换网模式是最适合的。经过几年 的发展,以太交换技术和产品都十分成熟,网络的实现和管理简单,维护量小,并且可 以向未来...

    微服务设计与解决方案.docx

    最早是应用是单块架构,后来为了具备一定的扩展和可靠性,就有了垂直架构,也就是加了个负载均衡,接下来是前几年比较火的SOA,主要讲了应用系统之间如何集成和互通,而到现在的微服务架构则是进一步在探讨一个应用...

    数据库设计-ER图.pdf

    同时,由于设计时要同时考虑多方面的问题,也使设计 工作变得十分复杂。1976 年 P.P.S.Chen 提出在逻辑结构 设计之前先设计一个概念模型,并提出了数据库设计的实体--联系方法 (Entity--Relationship Approach)。...

Global site tag (gtag.js) - Google Analytics