设计模式(四)单例模式

最后更新于:2022-04-01 11:05:11

**写在前边:**辛辛苦苦写了好几天终于能有一篇发到首页上了,其中的艰辛就不必多说了,我不是专家不发能首页,好多文章博乐也都不看,比起首页上那些空洞无味的文章,我觉得我的博客对一部分人能起到帮助的作用,如果您觉得我写的还可以就顶一下吧,您的支持是我最大的动力! ### 一、模式定义 单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它 提供全局访问的方法。 单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式 是一种对象创建型模式。单例模式又名单件模式或单态模式。 ### 二、模式结构 ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-20_57175852e1d94.jpg "") 我们可以看到单例模式与我们之前所讲的工厂模式不同,它只有一个Singleton角色,并且这个类自行创建自己的实例,并向系统提供这个实 例,可以注意到我没有在图中吧实例的建造个数限制为1个,这一点我们后边会讲到多例模式。 单例模式分为懒汉式的和饿汉式,有的地方也会讲登记式的单例模式,下面我们就一起来学习一下这三种单例模式。 ### 三、饿汉式的单例模式 饿汉式单例模式是Java语言中实现起来最为简单的单例模式,我们通过代码来看一下。 ~~~ package com.designpattern.singleton; public class EagerSingleton { /** * 直接创建一个本类的对象 */ private static final EagerSingleton eagerSingleton = new EagerSingleton(); /** * 覆盖默认的构造方法,将默认的构造方法声明为私有的,防止其他类直接创建对象 */ private EagerSingleton(){} /** * 提供一个工厂方法来返回本类的唯一对象 * @return */ public static EagerSingleton getInstance(){ return eagerSingleton; } } ~~~ 饿汉式单例模式就是一开始就自己创建一个私有的静态的本类对象,当这个类被加载时,静态变量eagerSingleton就会被初始化,这时这个 类的私有构造方法就会被调用。这个时候,单例类的唯一实例就被创建出来了。但需要获得单例类的对象是就调用getInstance方法。 需要注意的一点就是单例类的构造方法一定要声明为私有的,否则其他类就可以利用构造方法直接创建对象,使单例类不再是只有一个唯一 的实例。 另外,值得一提的是,由于构造方法是私有的,因此此类不能被继承。 饿汉式单例模式在自己加载时就将自己实例化,所以从资源利用效率的角度来讲,饿汉式单例模式不如懒汉式单例模式节省资源,但是饿汉 式单例模式的速度更快一些。 ### 四、懒汉式的单例模式 懒汉式单例模式与饿汉式稍有不同,下面看一下代码: ~~~ package com.designpattern.singleton; public class LazySingleton { /** * 单例类的唯一实例,但是不是加载时初始化 */ private static LazySingleton lazySingleton = null; /** * 覆盖原有的默认构造方法,声明为私有的,防止其他类直接使用构造方法创建单例类对象,同时也使子类无法继承 */ private LazySingleton() { } /** * 线程互斥的获取实例 * @return */ public static synchronized LazySingleton getInstance() { /** * 如果实例为空就创建实例,创建的语句只会执行一次 */ if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } } ~~~ 从代码我们可以看出,懒汉式单例模式的创建对象是在第一次企图获得单例类的对象时。另外我们再getInstance方法中使用了 synchronization关键字,这是防止出现race condition,使两个线程new出来两个单例类的实例。构造方法同样是私有的,这也决定了子类 不能继承自这个类。 懒汉式单例模式与饿汉式单例模式相比,更节省资源,但是必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作 为资源控制器在实例化时必然涉及资源初始化,而资源初始化很有可能耗费时间,这就意味着出现多个线程同时首次引用此类的几率变得较 大。 ### 五、登记式的单例模式 为了克服饿汉式单例模式以及懒汉式单例模式类均不可继承的缺点,产生了一种新的模式——登记式单例模式。 登记式的单例模式中父类中有一个集合,用来存储所有的子类的实例,当一个子类创建时,必须在父类的中登记,也就是把自己的实例加入 到父类的集合中,当其他类想要获取子类的实例时,就到父类的集合中查找,找到了就返回,如果找不到就创建这个子类的唯一实例。 这是父类的代码。 ~~~ package com.designpattern.singleton; import java.util.HashMap; public class RegSingleton { /** * 建立一个HashMap来存储子类的完整类名和子类的实例 */ private static HashMap registry = new HashMap<String, RegSingleton>(); /** * 首先将本类的实例加入到HashMap中 */ static{ RegSingleton x = new RegSingleton(); registry.put(x.getClass().getName(), x); } /** * 构造方法不再是private的了,所以子类可以继承了 */ protected RegSingleton(){ } /** * 根据子类传来的类名返回相应的实例 * @param name 想要获得的类的完整类名 * @return */ public static RegSingleton getInstance(String name){ /** * 提供默认的类 */ if(name == null){ name = "com.designpattern.singleton.RegSingleton"; } /** * 第一次引用这个类时创建类的实例,利用了反射机制 */ if(registry.get(name) == null){ try{ registry.put(name, Class.forName(name).newInstance()); }catch(Exception e){ e.printStackTrace(); } } /** * 返回子类想要的类的实例 */ return (RegSingleton)registry.get(name); } } ~~~ 这是子类的代码。 ~~~ package com.designpattern.singleton; public class RegSingletonChild extends RegSingleton{ /** * 构造方法必须是公有的,否则父类无法产生子类的对象 */ public RegSingletonChild(){} /** * 工厂方法,获取本类的唯一实例,实际上是借助了父类的getInstance方法 * @return */ public static RegSingletonChild getInstance(){ return (RegSingletonChild)RegSingleton.getInstance("com.designpattern.singleton.RegSingletonChild"); } } ~~~ 登记式的单例模式解决了懒汉式和饿汉式不能继承的缺点,但是子类中的构造方法变为了public的,所以其他类可以直接通过构造方法创建 类的实例而不用向父类中登记,这是登记式单例模式最大的缺点。 ### 六、什么情况下使用单例模式 - 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。 - 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。 - 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进, 使之成为多例模式 可能有人会将一个系统中需要的“全局”变量放在一个单例类中,这样做是不对的。首先,单例模式针对的是只能有一个实例的类而不是变 量,其次一个设计得当的系统就不应该有所谓的“全局”变量,这些变量应该放到他们所描述的实体所对应的类中去。将这些变量从他们的 实体类中抽出来,放到一个不相干的单例类中去,使得这些变量产生错误的依赖关系和耦合关系。 在资源方面管理方面,单例模式用的更多,比如一台计算机连接着一台打印机,那么这个打印机就只有一台,也只应该有一个实例,试想如 果有两个打印机的实例,这两个实例同时操控打印机会出现什么情况,打出来的东西将是乱七八糟的。 所以应该在合适的情况下合理的使用设计模式,而不是一味的追求一个系统利用了多少模式。 ### 七、单例模式的优点和缺点 **单例模式的优点:** - 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提 供了共享的概念。 - 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。 - 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。 **单例模式的缺点:** - 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。 - 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色, 包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。 - 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池 溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认 为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。 ### 八、模式在JDK中的应用 Java中的Runtime对象就是一个使用单例模式的例子。在每一个Java应用程序里面,都有唯一的一个Runtime对象,通过这个对象应用程序可 以与其运行环境发生相互作用。 Runtime类提供一个静态工厂方法getRuntime(): `public static Runtime getRuntime();` 通过调用次方法,可以获得Runtime类唯一的一个实例: `Runtime rt = Runtime.getRuntime();` ### 九、扩展:多例模式 单例是只有一个类实例,自然多例模式就是有多个类的实例了。下面就以有两个实例为例讲一下。 ~~~ package com.designpattern.singleton; import java.util.Random; public class Die { /** * 两个类实例 */ private static Die die1 = new Die(); private static Die die2 = new Die(); /** * 私有的构造方法 */ private Die(){} /** * 根据用户使用的标号来决定返回哪一个对象 * @param witchOne * @return */ public static Die getInstance(int witchOne){ if(witchOne == 1) return die1; else return die2; } /** * 使用对象产生随机数 * @return */ public synchronized int dice(){ Random rand = new Random(); return rand.nextInt(6)+1; } } ~~~ ~~~ package com.designpattern.singleton; public class Client { public static void main(String[] args){ Die die1 = Die.getInstance(1); Die die2 = Die.getInstance(2); System.out.println(die1.dice()); System.out.println(die2.dice()); } } ~~~ 我们看到多例模式的多例类中有多于一个对象实例,究竟返回哪一个对象取决于程序的业务逻辑。 源码下载:[http://download.csdn.net/detail/xingjiarong/9297315](http://download.csdn.net/detail/xingjiarong/9297315)
';