单例模式是什么?
单例模式(Singleton Pattern)是一种创建型的设计模式,它的目标是确保某一个类在 JVM 中有且仅有一个实例对象,并且这个类会提供一个全局的访问点。在 Java 中,有多种方式可以实现单例模式,下面将逐一介绍。
饿汉式单例
对于普通的类而言,实现单例通常需要确保以下三点:
- 构造器私有化;
- 静态的字段引用唯一的实例;
- 提供一个静态的工厂方法来获取该实例。
饿汉式单例指的是在装载单例类时就实例化该单例对象,例如:
public class HungrySingleton {
private HungrySingleton() { }
private static HungrySingleton singleton = new HungrySingleton();
public static HungrySingleton getInstance() {
return singleton;
}
// other fields and methods...
}
这种方式的优点是实现简单,缺点是可能会造成资源的浪费。因为 JVM 在装载 HungrySingleton 类时就会去实例化 singleton 对象,如果程序并不需要使用 singleton 对象,那么 singleton 对象在实例化过程中占用的资源以及消耗的时间则都是浪费的。
懒汉式单例
不同于饿汉式单例,懒汉式单例是以延迟加载的方式,在初次获取单例对象时,才去实例化它。例如:
public class LazySingleton {
private LazySingleton() { }
private static LazySingleton singleton;
public synchronized static LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
// other fields and methods...
}
为了确保线程安全,我们使用了 synchronized 关键字修饰了静态工厂方法。虽然懒汉式单例解决了资源浪费的问题,但它却可能引起新的问题:执行效率低。由于 synchronized 修饰的是 getInstance 整个方法,如果该方法被频繁调用,频繁的线程同步将会严重影响性能。
双检锁单例
为了解决上述由 synchronized 修饰整个静态工厂方法引起的性能问题,我们采取优化措施:先尝试获取单例对象,如果获取不到实例,才去获取锁以实例化对象,在获得锁之后需要再进行一次检查,以保证操作的原子性。例如:
public class DclSingleton {
private DclSingleton() { }
private static volatile DclSingleton singleton;
public static DclSingleton getInstance() {
if (singleton == null) {
synchronized (DclSingleton.class) {
if (singleton == null) {
singleton = new DclSingleton();
}
}
}
return singleton;
}
// other fields and methods...
}
注意,采用这种方式单例,必须用 volatile 修饰实例字段 singleton 以避免缓存不一致的问题。虽然这种方式有效地规避了线程安全和性能的问题,但它也存在缺陷:
- 使用了 volatile 关键字,在 Java 1.4 及以下版本不可采用此方式;
- 代码相当冗长且难以阅读。
静态内部类单例
上述的实现方式各有利弊,现在将介绍一种更为优雅的实现方式,利用静态内部类达到延迟加载的效果,同时避免繁琐的线程锁操作。
public class StaticInnerSingleton {
private StaticInnerSingleton() { }
private static class SingletonHolder {
public static final StaticInnerSingleton instance = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.instance;
}
// other fields and methods...
}
这种实现方式汲取了以上三种方式的优点:实现简单、延迟加载、执行高效以及线程安全。
枚举单例
上述介绍的方式都是利用私有的构造器实现单例,当要使得类可序列化时,仅仅在声明中加上 implements Serializable
是不够的。为了确保单例,必须将所有的成员字段都声明为瞬时的(transient),并且提供一个
readResolve 方法。否则,每次反序列化时,都会创建一个新的实例。
private Object readResolve() {
return singleton;
}
Joshua Block 在《Effective Java》一书中,提出了一种更为巧妙的方式:以枚举实现单例。
public enum EnumSingleton {
INSTANCE("VALUE");
private String value;
private EnumSingleton(String value) {
this.value = value;
}
// getter and setter
}
这种方法非常简洁,而且无偿地提供了序列化机制,绝对防止多次实例化。
参考资料
以下是本文参考的资料:
- https://www.baeldung.com/creational-design-patterns#singleton-pattern
- https://www.baeldung.com/java-singleton
- https://www.baeldung.com/java-singleton-double-checked-locking
- https://refactoring.guru/design-patterns/builder
- https://refactoringguru.cn/design-patterns/singleton
- https://sourcemaking.com/design_patterns/singleton