《Effective Java》中的第3条编程法则主要是针对在开发过程如何实现单例模式,作者 Joshua Bloch 在书中给出了3种单例模式的实现方式:私有构造器和公有静态域、私有构造器和公有静态方法、枚举式。
什么是单例模式?
单例模式是一种设计模式,旨在确保一个类只有一个实例,其主要目的是控制类的实例化过程,避免创建多个对象实例;并提供一个全局访问点来获取该实例。这种模式适用于需要唯一对象来协调全局操作或资源管理的场景,例如配置管理器、线程池等。
单例实现方式
实现方式一:私有构造器和公有静态域(饿汉式单例模式)
通过私有构造器和公有静态域实现单例模式是一种简单且直接的方法,这种实现通常被称为饿汉式单例模式。
public class Singleton {// 单例对象,在类加载时创建public static final Singleton INSTANCE = new Singleton();// 私有构造器,防止外部实例化private Singleton() {// 可以添加初始化代码}// 其他方法public void doSomething() {// 实现具体的业务逻辑}
}
在这种实现中,通过将构造器设为 private
,类外部无法直接创建实例,从而确保了单例模式的唯一性。而 static final
修饰符确保了在类加载阶段就创建并初始化实例,并且实例一旦创建后不可变,这样就进一步保证了单例实例的唯一性和一致性。整体设计既简单又有效。
实现方式二:私有构造器和公有静态方法(懒汉式单例模式 & 双重检查锁定)
私有构造器和公有静态方法的实现通常称为懒汉式单例模式:
public class Singleton {private static Singleton instance;private Singleton() {// 私有构造器}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
相比饿汉式单例模式实例的创建时机固定,懒汉式的实现将实例的初始化延迟到在第一次调用 getInstance
方法时创建,这样可以避免在程序启动时就初始化实例,从而节省资源。
但是在懒汉式的实现中,使用了synchronized
关键字保证在多线程环境下对 getInstance
方法的访问是线程安全的。然而,这种实现方式的缺点是每次调用 getInstance
方法时都要进行同步,这会带来性能开销。
幸运的是,由于我们使用静态工厂方法创建类的实例,那么我们就可以在方法种控制创建实例的时机,即在懒汉式单例模式的基础上进行优化,减少同步的开销来提高效率:
- 在获取实例时,首先检查实例是否已存在,如果已存在,则直接返回该实例。
- 如果实例不存在,再进行同步检查,以确保实例初始化的线程安全。
public class Singleton {// 使用 volatile 关键字确保线程安全private static volatile Singleton instance;// 私有构造器防止外部实例化private Singleton() {// 私有构造器}// 提供公共的静态方法获取实例public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton();}}}return instance;}
}
这种优化在获取实例时引入了两次检查,因此也被称为双重检查锁定:
- 第一次检查:在同步块外部进行,以减少不必要的同步开销。
- 第二次检查:在同步块内部进行,以确保实例的唯一性。
实现方式三:枚举式
由于枚举类型本身设计用于定义一组常量,因此实现单例模式时,通常一个枚举类型中只定义一个枚举实例:
public enum Singleton {INSTANCE;public void doSomething() {// 实现具体功能}
}
饿汉式 VS 懒汉式 VS 枚举式
饿汉式和懒汉式单例模式的实现,在反序列化和反射攻击可能会导致创建多个实例,破坏单例模式的唯一性。因此还需要添加以下处理:
-
实现
readResolve
方法,确保在反序列化过程中,获取的对象仍然是单例实例,而不是新的实例:private Object readResolve() {return getInstance(); }
-
在构造函数中增加检查,防止通过反射创建多个实例:
private Singleton() {if (instance != null) {throw new RuntimeException("Use getInstance() method to get the single instance of this class.");} }
枚举由于自身的特殊机制,使得枚举式相比前两者更适合单例模式得实现:
- 天然的线程安全:Java 枚举类型的实例在 JVM 加载时会创建一次且只创建一次,整个加载过程是线程安全的。因此,不需要额外的同步来保证线程安全。
- 防止反序列化攻击:枚举的实例由 JVM 管理,序列化和反序列化过程中保持唯一性。这意味着即使序列化和反序列化操作被恶意操控,也不会生成新的实例。
- 防止反射攻击:枚举类型的构造函数是私有的,当尝试使用反射创建枚举实例会抛出
IllegalArgumentException
异常,从而保护了单例的唯一性。
这也是 Joshua Bloch 在书中提到单元素的枚举类型经常成为实现 Singleton 的最佳方法的原因。