单例模式也被成为单件模式(或单体模式),主要作用是控制某个类型的实例数量为一个,而且只有一个。
很多时候,我们需要在应用中保存一个唯一的实例,例如后台服务进程需要一个全局的计数器,记录用户访问各个页面的点击数量。如果计数器数量非常多,那么必然会在计数过程中出现冲突的情况。为了尽量减少冲突,一个最简单的办法是只有一个计数器实例,所有计数工作都通过它来完成。
单例模式实现方式
实现单例模式的方式有很多,大体上可以分为外部方式和内部方式两种。
(1)外部方式
外部方式就是外部的客户程序使用某些全局对象时,做一些“Try-User”工作,简单说就是试着获取对象实例,如果没有,会获取不到,就自己创建一个,然后把它放在全局的位置上,然后再用,如果本来就有,则直接拿一个现成的来用。
(2)内部方式
内部方式就是类内部自己控制生成实例的数量,无论外部程序是否尝试过,类型自己先生成一个实例,然后控制只提供这一个实例,客户程序使用的都是这一个现成的实例。
但是随着集群和多核技术的普及,想通过简单的类型内部控制实现真正的单例模式越来越难,通过经典的单例模式实现分布式环境下的单例并不现实。因此本文所说的单例是有范围限制的。
单利模式特点
单例模式和工厂模式一样,是一种对象的创建模式。有如下几个特点:
1)该类只有一个实例
2)该类在自己内部自行创建自身的实例对象
3)向整个系统公开或者这个实例的接口
简单的单例模式代码实现如下:
可以看到,代码中定义了一个私有的构造器,不允许外界访问,这样外界无法通过new创建该类对象,只能在类内部new。instance就是在类内部实例化的一个对象,可以想到在全系统中也只有这一个,然后再通过getInstance这个静态方法获取:
Demo demo = Demo.getInstance();
Demo单例类在类被加载时,instance就会被初始化,即便加载器是静态的。如果初始化一个单例对象需要的开销大,那么在使用之前最合理的做法是不实例化,将实例化这个动作推迟到使用的时候,这就是惰性加载,惰性加载常常用于需要加载大量数据的单体。简单修改后的代码如下:
可以看到只是加了一个是否为null的判断,惰性加载的使用方法与普通加载完全一样。
单例模式的功能
单例模式的功能是保证这个类在运行期间只会被创建一个类实例,另外单例模式还提供了一个全局唯一访问这个类实例的访问点,就是getInstance方法。不管是上面的普通加载还是惰性加载,这个全局的唯一访问点是一样的。对于单例模式而言,不管采用何种实现方式,它都只关心类实例的创建问题,并不关心具体的业务功能。
单例模式的范围
单例类在什么范围内可以叫单例类? 目前Java里面实现的单例是一个ClassLoader及其子ClassLoader的范围。因为ClassLoader在普通加载模式下实现单例类时,会相应的创建一个类实例。
这说明如果一个虚拟机里面有很多ClassLoader,而且这些ClassLoader都装载某个类的话,就算这个类是单例,它也会产生很多个实例。如果一个机器上有多个虚拟机,那么每个虚拟机里面都至少有一个这个类的实例。也就是整个机器上就有很多个实例,这个类更不会是单例了。
另外需要注意,普通的单例模式并不适用于集群环境。
单例模式的命名
一般建议获取单例的方法命名为getInstance(),看到这个方法就知道这个方法返回类型肯定是单例类的实例了。getInstance方法可以有参数,要根据私有构造器的具体情况确定,在大多数情况下是没有参数的。单例模式的名称因为翻译的不同,也可以叫单件模式,单体模式等等。
单例模式的种类大致分为三种,分别是懒汉式单例,饿汉式单例和登记式单例。在下面的内容中会分别介绍。