type
status
date
slug
summary
tags
category
icon
password
意图
确保一个类只有一个实例,并提供该实例的全局访问点
它的优点是:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)
- 避免对资源的多重占用而出现并发问题(比如写文件操作)
缺点是:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
在很多方面我们都需要用到单例模式(表示全局唯一类),比如:配置文件类、数据库连接池类和 ID 生成器类
类图
接下来的五种单例都是为了确保一个类只有一个实例,并提供该实例的全局访问点,他们的共同点是:
- 使用一个私有构造方法、一个私有静态变量以及一个公有静态方法来实现
- 私有构造方法保证了不能通过构造函数来创建对象实例,只能通过公有静态方法返回唯一的私有静态变量。
虽然他们实现方式虽然过程各有不同,但其差别无非是下面几点:
- 创建对象时是否线程安全
- 是否支持延迟加载
- 使用 getInstance 时性能高低(是否加锁)
饿汉式
这种实现方式不支持延时加载,好处是在紧急情况下不会影响初始化实例对象创建的速度,缺点是刚开始就初始化实例对象会影响系统性能,比如:占用系统内存、加载配置文件导致系统启动时间延长等。没有好与坏的区分,结合项目实际情况进行使用。
懒汉式
如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
双重检测
由于编译优化(指令重排)可能导致的有序性问题会造成 NullPointerException 。要解决这个问题,我们需要给 INSTANCE 成员变量加上 volatile 关键字,禁止指令重排序才行。实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。
静态内部类
有点类似饿汉式实现方式,目前看来性能最优也最简单的一种实现方式了。使用静态内部类的优点是:外部类加载时不需要立即加载内部类,内部类不被加载则不去初始化 INSTANCE,故而不占内存。只有当 getInstance 方法被调用时才会初始化 INSTANCE。
枚举
最简单的单例模式实现方式,没有之一!该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止多次实例化的代码。该实现(枚举对象)是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。
实战案例
配置信息类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。
唯一递增 ID 号码生成器:如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。
替代方案
为了保证全局唯一,除了使用单例,我们还可以用静态方法(可以解决单例隐藏类之间依赖关系)或者依赖注入(可以解决单例不支持有参构造问题)来实现。不过,对于单例存在的其他问题,比如对 OOP 特性、扩展性、可测性不友好等问题,还是无法解决。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由开发者自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。