课程咨询 :186 8716 1620      qq:2066486918

昆明Java培训 > 达内新闻 > java程序员:设计模式之单例模式
  • java程序员:设计模式之单例模式

    发布:昆明Java培训      来源:达内新闻      时间:2016-09-28

  • 昆明达内Java培训的老师知道单例模式是软件开发中非常普遍的一种模式。它的主要作用是确保系统中,始终只存在一个类的实例对象。

    这样做的好处有两点:

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

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

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

    下面昆明达内Java培训的老师依次介绍开发中常用的几种实现方式,以及他们的优缺点:

    最简单的形式:

    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都会加载该类 。

    为了解决这个问题,昆明达内Java培训的老师发现开发者们后来又想到了一种延时加载的方法:

    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仍然会调用构造函数。从而破坏单例的唯一性。

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

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

    昆明达内Java培训的老师个人建议,如果是服务端的话(客户端则更多的需要根据使用场景来斟酌),建议使用第一种。原因如下:

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

    ps该种方法并不适用于在JDK1.5之前,这并不是由于语法的错误,而是由于java的内存模型自身的问题:简而言之就是,由于jvm指令顺序的优化,可能会导致先给instance赋予了一段堆内存,然后才在该堆内存上初始化该对象。在instan ce变量赋值成功后,退出同步代码块。新线程进入判断条件,发现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 = ew 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自身的机制,很好的解决这些问题。

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

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

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

    了解详情请登陆昆明达内Java培训官网(km.Java.tedu.cn)!

    推荐文章

上一篇:Spring提供解决方案

下一篇:[javaSE知识]反射-Class类的基本操作

最新开班日期  |  更多

Java--零基础全日制班

Java--零基础全日制班

开班日期:11/30

Java--零基础业余班

Java--零基础业余班

开班日期:11/30

Java--周末提升班

Java--周末提升班

开班日期:11/30

Java--零基础周末班

Java--零基础周末班

开班日期:11/30

  • 网址:http://km .java.tedu.cn      地址:昆明市官渡区春城路62号证券大厦附楼6楼
  • 课程培训电话:186 8716 1620      qq:2066486918    全国服务监督电话:400-827-0010
  • 服务邮箱 ts@tedu.cn
  • 2001-2016 达内国际公司(TARENA INTERNATIONAL,INC.) 版权所有 京ICP证08000853号-56