zoukankan      html  css  js  c++  java
  • 设计模式之单例模式

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

    这样做的好处有两点:

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

     

  • 相关阅读:
    Centos7安装vsftpd
    Centos7安装FTP突然无法登录
    关于limit_req和limit_conn的区别
    linux nginx大量TIME_WAIT的解决办法--转
    discuz过滤词语无效
    XMReport相关视频资料
    XMReport富文本编辑器上线啦!!! 小小莫_6ada
    mysql/mybatis insertBatch插入速度太慢了
    TCP学习笔记
    Apache Derby ij下面连接Embedded数据库
  • 原文地址:https://www.cnblogs.com/jilodream/p/5782511.html
Copyright © 2011-2022 走看看