动机
在某一些程序中,只有一个实例非常重要。
处理资源访问冲突。
例如,我们需要打印东西,只有一台打印机,每次只能有一个打印任务。
表示全局唯一的类
定义
单例模式是对象创建型的一种。它确保一个类只有一个实例,而且自行实例化,并向整个系统提供这个实例。这个类可以保证没有其他实例被创建,而且他可以提供一个访问的方法。
单例模式三个要点:
- 一个类只能有一个实例;
- 他必须自行创建这个实例;
- 他必须自行向整个系统提供这个实例;
单例的实现
单例有下面几种经典的实现方式。
饿汉式
饿汉式,是在类加载的期间,就已经将instance静态实例初始化好了,所以,instance实例,的创建是现成安全的,不过,这样的实现方式不支持延迟加载实例。
懒汉式
懒汉式将方法增加同步锁,相对饿汉式的优势就是这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁调用会导致性能瓶颈。
双重检测
双重检测是一种既支持延迟加载,又支持高并发的单例实现方式,在创建instance的地方加锁。只要instance被创建之后,再调用getInstance()函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。
静态内部类
利用Java静态内部类来实现单例,这种实现方法,既支持延迟加载,也支持高并发,实现起来比双重检测简单。
枚举
最简单的实现方式,基于枚举类型实现,这种实现方式通过Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
存在的问题
尽管单例是一个很常用的设计模式,但是有些人称之为反模式
。那它到底存在哪些问题呢?
单例对OOP特性的支持不友好
单例的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解OOP的抽象特性。对不同需求的变化,需要修改所有用到的地方,改动影响面比较大。除此之外,对继承和多态也不友好,损失了拓展性。
单例会隐藏类之间的依赖关系
单例类不需要显示创建、不需要依赖参数传递,在函数中可以直接历来,这种调用关系就比较复杂。我们需要查看每个函数的实现,才知道有没有依赖这个单例类。可以使用依赖注入的方式来解决。
单例对代码的扩展性不友好
单例类只能有一个实例。如果某天,我们需要在代码中创建两个实例,就需要比较大的改动。比如在系统设计的初期,系统中只应有一个数据库连接池,随着系统中一些慢查询语句的产生,导致对数据库连接长时间占用,因此需要单独隔离出一个数据库连接池,来避免影响执行效率。
单例对代码的可测试性不友好
如果单例类依赖比较重的外部资源,我们在写单元测试的时候,希望可以通过
mock
(构造虚拟对象来以便测试)的方法替换掉,但是单例这种模式,就导致无法替换。单例不支持有参数的构造函数
比如我们创建一个连接池的单例对象,我们没办法通过参数来指定连接池的大小。解决方案见代码(IDGeneratorWithParams)。
替换解决方案
通过IOC容器,或者工厂模式。静态类静态方法来做单例的替换。
如何理解为单例模式的唯一性
单例的定义中,一个类只允许创建唯一一个对象,那么,这个唯一性的作用范围是什么呢?
是线程内唯一?还是进程内唯一?如何实现呢?
线程内唯一的实现方式
可以通过一个HashMap来存储对象,key是线程ID,value是对象,这样就可以做到不同的线程对应不同的对象了。也可以使用Java自身提供的ThreadLocal工具类,来实现线程内唯一的实例。实现详见代码。
进程内唯一的实现方式
我们需要把这个实例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区将它读到内存,并反序列化成对象,然后再使用,使用完成之后,还需要再存储回外部共享存储区。因此需要对对象进行加锁。在进程使用完这个对象之后,还需要显式将对象从内存中删除,并且释放对对象的枷锁。
当唯一性的作用范围是进程时,实际上,他的作用范围并非进程,而是类加载器(Class Loader)。
Class Loader有两个作用:1.将class类加载到JVM中;2.确认每个类应该由哪个类加载器加载,并用于判断JVM运行时两个类是否相等。双亲委派模型通过优先委派给父类加载器加载来保证了类在内存中的唯一性,也就是class Loader内的唯一。所以这里所说单例类实例唯一性的作用范围是类加载器指的就是即使类全名相同的类文件也必须要保证被同个类应用类加载器加载。
多例模式
相对于单例,多例是指一个类可以创建有限个数的对象。通过静态代码块来初始化对象实例。
还有一种理解方式就是同一类型的只能创建一个对象,不同类型的可以创建多个对象。实现方式也可以通过线程内唯一的思路,增加HashMap,key是类型,而value是对象。