(单例是创建型模式的一种。创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。其中,单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。)
首先为什么要使用单例?
单例模式在解决资源竞争问题上,相比其他方法(例如:类级别锁,分布式锁,并发队列 BlockingQueue等),不用创建那么多 对象,一方面节省内存空间,另一方面节省系统文件句柄。
另外,从业务上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。比如,系统的配置信息类。
如何实现一个单例?
1.构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
2.考虑对象创建时的线程安全问题;
3.考虑是否支持延迟加载;
4.考虑 getInstance() 性能是否高(是否加锁)。
以一个生成随机ID的类为例
1. 饿汉式
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;
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
懒汉式的缺点是给 getInstance() 方法加了锁(synchronzed),导致这个函数的并发度很低,相当于串行操作。而这个函数是在单例使用期间,一直会被调用。如果这个单例频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
3. 双重检测
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
有人说,这种实现方式有问题,可能因为指令重排序导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。
要解决这个问题,需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。
实际上高版本的 Java 已经解决了这个问题(解决的方法很简单,只要把对象 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。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
5. 枚举
一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}