定义
单例模式是属于创建型模式;
其英文定义的原话是:Ensure a class has only one instance,and provide a global point of access to it.
我们通俗点理解就是:一个类只允许创建一个对象(或者实例),那这个类就是一个单例类。
如何实现
单例类的实现也非常的简单,实现一个简单的单例类,一般需要注意以下几点:
- 构造函数需要是 private 访问权限,这样才能避免外部通过 new 创建实例;
- 考虑对象创建时的线程安全问题;
- 考虑是否支持延迟加载;
- 考虑 getInstance() 性能是否高(是否加锁)。
下面Java语言实现单例的几种常见写法
1.饿汉式
饿汉式的实现方式比较简单。一个“饿”字的精髓就体现于在类加载的时候,instance 静态实例就已经创建并初始化好了。不过,这种方式的缺点也很明显,那就是不支持延迟加载,但是instance 实例的创建过程是线程安全的。
下面是一个简单的单例ID生成器具体的Java实现
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();// 饿汉式的精髓
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就可以避免在程序运行的时候,再去初始化导致的性能问题。
2.懒汉式
懒汉式和饿汉式的思想差不多,其相对于饿汉式的优势就是支持延迟加载。
具体的代码实现如下所示:
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance; // 懒汉式,调用getInstance时才实例化
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
这种懒汉式的缺点也很明显,给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度会很低,在使用这个单例的时候都将是串行操作。
3.双重检测
懒汉式是给获取实例的方法加了一把大锁,实际上我们只需要在第一次获取的时候需要去初始化类,后面都无需再初始化了,双重检测就是懒汉式的改进版本,即支持延迟加载、又支持高并发(锁粒度减小或无锁),在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了(第一次检测就已经初始化好了)。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) { //1. 第一次检测
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) { //2. 第二次检测
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
有人说,这种实现方式有些问题。因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。
要解决这个问题,我们可以给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。
4.静态内部类
再来看一种比双重检测更加简单的实现方法,利用 Java 的静态内部类。
它有点类似饿汉式,但又能做到了延迟加载。
看它的代码实现。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
SingletonHolder
是一个静态内部类,当外部类 IdGenerator
被加载的时候,并不会创建 SingletonHolder
实例对象。
只有当调用 getInstance()
方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。
所以,这种实现方法既保证了线程安全,又能做到延迟加载。
5. 枚举
最后介绍一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
具体的代码如下所示:
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
单例存在哪些问题?
- 单例对 OOP 特性的支持不友好 OOP 的四大特性是封装、抽象、继承、多态。单例这种设计模式对于其中的抽象、继承、多态都支持得不好。比如上面我们实现的IdGenerator,如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。
- 单例会隐藏类之间的依赖关系 单例无法通过构造函数、参数传递的方式来进行创建,无法通过函数的定义来查看类的依赖关系,具体实现逻辑都是隐藏在内部的,需要仔细查看代码实现才能知道这个类到底依赖哪些类。
- 单例对代码的扩展性不友好 如果以后我们要在程序中允许创建两个实例或者多个实例,则需要对单例模式做很大的改动,虽然这种需求很少,但并不是没有,比如线程池。
- 单例对代码的可测试性不友好 单例类这种硬编码式的使用方式,会导致在编写单元测试的时候很难实现 对依赖的外部资源进行mock 替换。
- 单例不支持有参数的构造函数 单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。 如何解决呢? 1、先调用 init(param) 方法初始化参数,然后调用 getInstance()获取实例。 2、将参数放到 getIntance() 方法中。 3、将参数放到另外一个全局变量中。
设计模式系列阅读目录
- 设计模式:创建型—单例模式
- 设计模式:创建型—原型模式
- 设计模式:创建型—工厂模式
- 设计模式:创建型—建造者模式
- 设计模式:结构型—享元模式
- 设计模式:结构型—组合模式
- 设计模式:结构型—代理模式
- 设计模式:结构型—桥接模式
- 设计模式:结构型—装饰器模式
- 设计模式:结构型—适配器模式
- 设计模式:结构型—门面模式
- 设计模式:行为型—中介模式
- 设计模式:行为型—命令模式
- 设计模式:行为型—备忘录模式
- 设计模式:行为型—模板模式
- 设计模式:行为型—状态模式
- 设计模式:行为型—策略模式
- 设计模式:行为型—职责链模式
- 设计模式:行为型—观察者模式
- 设计模式:行为型—解释器模式
- 设计模式:行为型—访问者模式
- 设计模式:行为型—迭代器模式