昆明java培训
达内昆明广州春城路

18487146383

热门课程

java程序员:设计模式之单例模式

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

昆明达内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赋予了一段堆内存,然后才在该堆内存上初始化该对象。在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 = 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类的基本操作

Effective java的创建和销毁对象

昆明达内Java培训学费多少?

LinkedIn服务:三人行,必有我师【达内java培训】

Java培训:7月排行榜Go创新高Java稳第一

选择城市和中心
贵州省

广西省

海南省

台湾