设计模式之单例模式

单例模式是软件开发中非常普遍的一种模式。它的主要作用是确保系统中,始终只存在一个类的实例对象。

这样做的好处有两点:

1、对于需要频繁使用的对象,在每次使用时,如果都需要重新创建,并且这些对象的内容都是一样的。则不但提高了jvm的性能开销(堆中开辟新地址,同时降低GC效率等),同时还会降低代码的运行效率。倘若始终在堆中只存在唯一的一个实例对象。任何方法在使用时,均直接访问这个实例对象,则大大提高了系统的运行效率。

2、可以更好的维护对象,倘若系统中存在多个相同的实例对象,而一旦这些实例对象的属性发生了改变,则需要通知系统中所有的实例对象均发生相同的改变,才能保证数据的有效性和唯一性。但是当系统的复杂度到达一定的量级后,维护这种场景的开销会越来越大。比如:如何通知到所有的类实例?或者出现多线程场景后,如何保证所有的实例对象的属(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )性状态保持同步修改?单例模式可以很好的解决这个问题,因为整个系统中,只存在一个该类的实例对象。

单例模式实现的核心就是,通过创建方法,始终返回的都是一个唯一的实例。

下面依次介绍开发中常用的几种实现方式,以及他们的优缺点:

最简单的形式:

 1 public class Singleton
 2 {
 3     private Singleton()
 4     {
 5         //do sth
 6 
 7     }
 8     
 9     private static Singleton instance=new Singleton();
10 
11     public static Singleton getInstance()
12     {
13         return instance;
14     }
15 }


这样实现单例模式的好处是,实现的逻辑简单,易于阅读和使用。缺点是由于instance使用的是类静态字段并且直接初始化,所以在jvm加载该类时,就会直接创建该实例。而我们或许始终都不会使用该实例。倘若示例中的构造函数do sth部分是非常耗时的部分,则会导致加载类的初期,系统的响应速度持续走高,并且在jvm堆中始终都会存在这(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )个对象实例,形成内存的浪费。

ps 有些人可能会很难理解,既然jvm加载该类时,就代表我们会使用该对象了,为什么还会存在该实例不会被使用的场景?这里举个例子,比如需要用到这个类的某个静态字段,或者静态方法或者这个类被反射到,jvm都会加载该类。

为了解决这个问题,开发者们后来又想到了一种延时加载的方法:

 1 public class Singleton
 2 {
 3     private Singleton()
 4     {
 5         //do sth
 6     }
 7 
 8     private static Singleton instance = null;
 9 
10     public static synchronized Singleton getInstance()
11     {
12         if(instance == null)
13         {
14             instance=new Singleton();
15         }
16         return instance;
17     }
18 }

之所以给这个方法加入一个同步保护,是由于可能存在多线程的场景,线程A首先进入获取实例的方法,判断instance为null,则开始运行构造函数,而线程B同时进入该方法,由于构造方法尚未运行结束,因此instance仍然为null,所以线程B仍然会调用构造函数。从而破坏单例的唯一性。

但是单例,势必会造成线程等待,我们让单例类的构造函数只运行一次,为的就是快,而现在反而又为了线程安全,使速度降下来。有些人或许会觉得一个小小的同步,影响性能并不大,可是如果出现高并发时,最后一个线程等待的时间,是之前线程等待时间的累加,《java程序性能优化》书中曾经做过尝试,在五个线程同时调用以上代码时,耗费时间是390ms,而非延时加载的方法(第一种方法)耗时为0ms(也就是未到达1个ms),两者相差甚多。

不延时,可能会让系统无用开销过多,而延时又为了保证线程安全,造成额外的开销,究竟应该使用哪种呢?

我个人建议,如果是服务端的话(客户端则更多的需要根据使用场景来斟酌),建议使用第一种。原因如下:

1)方法简洁,不容易出错。(这个我(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )认为非常重要,很多人可能觉得无所谓)

2)硬件现在越来越廉价,用空间换时间大部分情况下是非常划算的。

3)大部分客户端更关心的是服务器在运行期的响应时间,而非服务器在启动时的快慢。(这里的表述不太严谨)

尽管如此,我们还是希望又可以做到延时加载,又能不让线程存在等待。于是有人想到了以下的方式:

 1 public class Singleton
 2 {
 3     private Singleton()
 4     {
 5         //do sth
 6     }
 7 
 8     private static Singleton instance = null;
 9 
10     public static Singleton getInstance()
11     {
12         if(instance==null)
13         {
14             synchronized(Singleton.class)
15             {
16                 if(instance==null)
17                 {
18                     instance=new Singleton();
19                 }
20             }
21         }
22         return instance;
23     }
24 }

这样做的好处是,将线程等待的区间段缩减至最低,只在类初期初始化时,增加线程安全的保护。倘若已经创建成功,则再次获取实例的线程是不需要再次等待的。

个人不建议这种写法,因为看着别扭,不方便阅读,双重锁尽管使用广泛,但是毕竟第一次阅读时,还是需要仔细分析下,毕竟java中还有很多其他实现单例的优雅的方式。

ps 该种方法并不适用于在JDK1.5之前,这并不是由于语法的错误,而是由于java的内存模型自身的问题:简而言之就是,由于jvm指令顺序的优化,可能会导致先给instance赋予了一段堆内存,然后才在该堆内存上初始化该对象。在instance变量赋值成功后,退出同步代码块。新线程进入判断条件,发现instance仍然未初始化,所以再次开始初始化该(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )变量。导致instance被反复初始。在jdk1.5以后推出了volatile关键字,我们可以用该关键字修饰instance变量,从而防止jvm优化该段指令。

那么还有什么办法来解决这个方法呢?聪明的人想到了使用内部类来保存instance的持有。

 

 1 public class Singleton
 2 {
 3     private Singleton() 
 4     {
 5         // do sth
 6     }
 7 
 8     private static class SingletonInner
 9     {
10         private static Singleton instance = new Singleton();
11     }
12 
13     public static Singleton getInstance()
14     {
15         return SingletonInner.instance;
16     }
17 }

前文所述的例子,其实无外乎存在两个问题,第一最好使用延时加载,最好延时加载的时机是我真正要用到实例的时候,而非加载单例类的时候。第二,开始使用前,就已经加载好单例了,别让(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )我出现等待。

而静态内部类可以很好的解决这个问题:1加载该类的时候(调用静态字段,静态方法时),并不会调用构造函数创建实例。2真正需要实例时,实例是保存在在静态内部类中的字段的,静态内部类此时才会被加载,而单例类此时就会创建实例<clinit>()方法,所以多线程进入时,字段已经被初始化完毕了。这种形式的单例也是我非常喜欢的一种单例形式,不但阅读方便,同时还很好的弥补了其他单例的一些弊端。

最后再介绍一种利用关键字很好的解决了单例问题的方式:

什么关键字生来就可以保证一个实例而生的呢?这就是枚举。

先看代码

 1 public enum Singleton
 2 {
 3     instance();
 4     Singleton() 
 5     {
 6         // do sth
 7     }
 8 
 9     public final void A()
10     {
11 
12     }
13 }


了解枚举的人都知道每一个枚举项都是该类的一个实例,而该类也不可以再创造出其他更多的实例。同时通过反射和正反序列化的形式,其实是可以突破前文中示例的单例限制的,即创造出多个实例(虽然如此,我也没怎么见过需要各种防范这些问题的)。而使用枚举,可以通过java自身的机制,很好的解决这些问题。这也是《Effective java》(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )的作者非常建议的形式。不过尽管这本书非常畅销,而且评价很高,但是却很少见到使用这种写法的地方。

说了这么多,我们也应该再来谈谈单例模式的缺点:

1、单例模式不容易拓展,类的构造函数被私有化,子类根本无法执行父类的构造方法

2、开发过程中,为了尽可能的保证,单例一旦构造好,就可以方便直接使用的目的,往往在单例中加入大量的方法,从而使单例类的职责很模糊,很多功能无法界定是否应该由该类来负责,违反了面相对象的基本原则。

 

posted @ 2016-09-12 22:53  王若伊_恩赐解脱  阅读(897)  评论(5编辑  收藏  举报