设计模式之单例模式
Posted 算死草
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之单例模式相关的知识,希望对你有一定的参考价值。
单例模式是软件开发中非常普遍的一种模式。它的主要作用是确保系统中,始终只存在一个类的实例对象。
这样做的好处有两点:
1、对于需要频繁使用的对象,在每次使用时,如果都需要重新创建,并且这些对象的内容都是一样的。则不但提高了jvm的性能开销(堆中开辟新地址,同时降低GC效率等),同时还会降低代码的运行效率。倘若始终在堆中只存在唯一的一个实例对象。任何方法在使用时,均直接访问这个实例对象,则大大提高了系统的运行效率。
2、可以更好的维护对象,倘若系统中存在多个相同的实例对象,而一旦这些实例对象的属性发生了改变,则需要通知系统中所有的实例对象均发生相同的改变,才能保证数据的有效性和唯一性。但是当系统的复杂度到达一定的量级后,维护这种场景的开销会越来越大。比如:如何通知到所有的类实例?或者出现多线程场景后,如何保证所有的实例对象的属性状态保持同步修改?单例模式可以很好的解决这个问题,因为整个系统中,只存在一个该类的实例对象。
单例模式实现的核心就是,通过创建方法,始终返回的都是一个唯一的实例。
下面依次介绍开发中常用的几种实现方式,以及他们的优缺点:
最简单的形式:
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堆中始终都会存在这个对象实例,形成内存的浪费。
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)方法简洁,不容易出错。(这个我认为非常重要,很多人可能觉得无所谓)
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仍然未初始化,所以再次开始初始化该变量。导致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 }
前文所述的例子,其实无外乎存在两个问题,第一最好使用延时加载,最好延时加载的时机是我真正要用到实例的时候,而非加载单例类的时候。第二,开始使用前,就已经加载好单例了,别让我出现等待。
而静态内部类可以很好的解决这个问题: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》的作者非常建议的形式。不过尽管这本书非常畅销,而且评价很高,但是却很少见到使用这种写法的地方。
说了这么多,我们也应该再来谈谈单例模式的缺点:
1、单例模式不容易拓展,类的构造函数被私有化,子类根本无法执行父类的构造方法
2、开发过程中,为了尽可能的保证,单例一旦构造好,就可以方便直接使用的目的,往往在单例中加入大量的方法,从而使单例类的职责很模糊,很多功能无法界定是否应该由该类来负责,违反了面相对象的基本原则。
以上是关于设计模式之单例模式的主要内容,如果未能解决你的问题,请参考以下文章