单例模式(Singleton Pattern)是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这对于需要控制资源使用或需要集中管理某些资源的场景非常有用,例如配置文件读取器、数据库连接池管理器等。
1.单例模式的特点
- 唯一性:一个类只有一个实例。
- 全局访问:提供一个全局访问点来访问该实例。
2.实现单例模式的方法
2.1 饿汉式(Eager Initialization)
“饿汉式”(Eager Initialization)是一种在软件设计模式中用于单例模式(Singleton Pattern)的实例化方式。它之所以被称为“饿汉式”,是因为它在类加载时就立即创建实例,而不是在首次使用时才创建。这种方式的名称暗示了这种急切的行为。
特点
- 立即创建:在类加载的时候就创建实例,不管是否会用到该实例。
- 线程安全:由于实例在类加载时就已经创建,所以在多线程环境下不会有并发问题。
- 简单实现:实现起来比较简单,代码容易理解和维护。
缺点
- 资源浪费:饿汉式在类加载时就创建实例,不管这个实例是否会被使用。如果实例的创建过程比较耗资源,而实际应用中并不一定会用到这个实例,那么会造成资源浪费。
- 类加载时间变长:因为实例在类加载时就创建,如果实例的初始化过程比较复杂,会导致类加载时间变长,影响应用启动时间。
- 不支持延迟加载:饿汉式不能实现延迟加载(Lazy Loading),即只有在需要时才创建实例。如果实例创建过程很复杂并且使用频率较低,那么饿汉式不如懒汉式合适。
适用场景:
- 在应用程序启动时就需要实例,且不会浪费资源的场景。
示例代码:
public class Singleton {
// 在类加载时就创建实例
private static final Singleton instance = new Singleton();
// 私有构造函数,防止实例被创建
private Singleton() {}
// 提供获取实例的方法
public static Singleton getInstance() {
return instance;
}
// 示例方法
public void someMethod() {
System.out.println("Singleton instance method called.");
}
}
public class Main {
public static void main(String[] args) {
// 获取单例实例
Singleton singletonInstance = Singleton.getInstance();
// 使用单例实例进行操作
singletonInstance.someMethod();
}
}
在这个例子中,Singleton
类的实例在类加载时就被创建,并保存在静态变量 instance
中。通过调用 getInstance()
方法,外部代码可以获取这个唯一的实例。
2.2 懒汉式(Lazy Initialization)
这种方式在第一次调用 getInstance
时才创建实例,能够实现延迟加载,但需要注意线程安全问题。这种方式通过在需要时才创建实例,避免了饿汉式的资源浪费问题。然而,在多线程环境下需要特别注意线程安全,可以使用同步方法或双重检查锁定来保证实例的唯一性。
优点
- 延迟加载:实例在第一次使用时才创建,避免了不必要的资源浪费。
- 按需创建:只有在需要时才创建实例,适合初始化过程复杂或耗资源的情况。
缺点
- 线程不安全:上面的实现并没有考虑线程安全问题,在多线程环境下可能会创建多个实例。
适用场景:
- 单线程环境,或能保证实例方法调用时不会有多线程竞争的场景。
示例代码:
典型的懒汉式单例模式
public class Singleton {
// 静态变量,用于保存单例实例
private static Singleton instance;
// 私有构造函数,防止实例被创建
private Singleton() {}
// 提供获取实例的方法,第一次调用时实例化对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// 示例方法
public void someMethod() {
System.out.println("Singleton instance method called.");
}
}
线程安全的懒汉式单例模式
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void someMethod() {
System.out.println("Singleton instance method called.");
}
}
2.3 双重检查锁定(Double-Checked Locking)
这种方式结合了饿汉式和懒汉式的优点,既能实现延迟加载,又能保证高效的线程安全。它通过减少锁的范围来提高性能,同时确保实例在多线程环境下只被创建一次。双重检查锁定的关键是在第一次检查后再进行一次检查,以避免不必要的同步开销。
优点
- 高效:通过减少同步的范围,提高了性能。只有在实例未被创建时才进行同步,避免了每次获取实例时都进行同步。
- 线程安全:使用双重检查锁定确保了实例在多线程环境下只被创建一次,保证了线程安全性。
缺点:
- 实现较复杂,需要确保使用
volatile
关键字。
适用场景:
- 多线程环境,需要延迟加载和较高的访问性能的场景。
示例代码:
public class Singleton {
// 使用 volatile 确保 instance 变量的可见性
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 void someMethod() {
System.out.println("Singleton instance method called.");
}
}
public class Main {
public static void main(String[] args) {
// 创建多个线程同时获取单例实例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Singleton singletonInstance = Singleton.getInstance();
singletonInstance.someMethod();
}).start();
}
}
}
关键点解释
- volatile 关键字:
- 使用
volatile
确保instance
变量的可见性和有序性。volatile
保证了对变量的写操作对所有线程立即可见,防止了指令重排序的问题。
- 使用
- 第一次检查:
- 这个检查在进入同步块之前进行,用于避免不必要的同步开销。如果实例已经存在,则直接返回实例。
- 同步块:
synchronized (Singleton.class)
- 只有在第一次检查后
instance
仍为null
时,才进入同步块。这样可以确保只有在实例未被创建时才会进行同步,从而减少了同步开销。
- 第二次检查:
- 进入同步块后,再次检查
instance
是否为null
。这一步是必须的,因为在多线程环境下,可能有多个线程在第一次检查后同时进入同步块,此时需要再次检查确保实例只被创建一次。
- 进入同步块后,再次检查
##2.4 静态内部类(Static Inner Class)
静态内部类(Static Inner Class)是一种用于实现线程安全的懒汉式单例模式的方式。它利用类的加载机制,确保实例的延迟初始化和线程安全性,同时避免了同步带来的性能开销。
优点
- 延迟加载:只有在第一次调用
getInstance()
方法时,静态内部类才会被加载,从而实现延迟初始化。 - 线程安全:静态内部类的加载过程由 JVM 保证线程安全,不需要显式的同步。
- 高效:不需要同步机制,避免了同步带来的性能开销。
缺点:
- 没有明显缺点,被认为是最好的单例实现之一。
适用场景:
- 任何需要单例的场景,特别是需要延迟加载和线程安全的场景。
示例代码:
public class Singleton {
// 私有构造函数,防止实例被创建
private Singleton() {}
// 静态内部类,持有 Singleton 实例
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 提供获取实例的方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 示例方法
public void someMethod() {
System.out.println("Singleton instance method called.");
}
}
关键点解释
- 私有构造函数:
- 防止外部直接创建
Singleton
实例,确保单例模式。
- 防止外部直接创建
- 静态内部类:
private static class SingletonHolder
- 这是一个静态内部类,持有
Singleton
类的唯一实例。该内部类只有在调用getInstance()
方法时才会被加载。
- 实例的创建:
private static final Singleton INSTANCE = new Singleton();
- 在静态内部类中创建
Singleton
实例。由于静态变量的初始化是在类加载时进行的,JVM 会确保线程安全。
- 获取实例的方法:
public static Singleton getInstance()
- 通过调用
SingletonHolder.INSTANCE
返回单例实例。首次调用时会加载SingletonHolder
类,并初始化INSTANCE
。
2.5 枚举(Enum)单例模式
这种方式不仅能防止反射攻击,还能防止序列化导致的多实例问题,是实现单例模式的最佳实践。
为什么枚举适合单例模式?
- 线程安全:枚举类型本身就是线程安全的,因此不需要显式的同步处理。
- 防止反射攻击:枚举类不能通过反射创建实例。
- 防止序列化和反序列化问题:枚举类型在序列化和反序列化时能够保证实例的唯一性。
- 简洁和易读:代码简洁,容易理解和维护。
缺点:
- 灵活性稍差,不能延迟加载(不过枚举单例通常不需要延迟加载)。
适用场景:
- 需要全局唯一实例且不需要延迟加载的场景。
示例代码:
枚举的实现单例模式完整示例:
public enum Singleton {
INSTANCE;
// 单例实例的其他方法
public void someMethod() {
// 方法实现
System.out.println("This is a singleton method.");
}
}
如何使用上面的枚举单例:
public class SingletonTest {
public static void main(String[] args) {
// 访问单例实例
Singleton singleton = Singleton.INSTANCE;
// 调用单例实例的方法
singleton.someMethod();
}
}
详细解析
- 定义枚举类型:
public enum Singleton
定义了一个枚举类型Singleton
。 - 单一实例:
INSTANCE
是Singleton
枚举的唯一实例。通过Singleton.INSTANCE
可以访问该实例。 - 方法:枚举类中可以定义方法,例如
someMethod
,可以通过Singleton.INSTANCE.someMethod()
来调用。
枚举单例的应用场景
枚举单例模式非常适合用于以下场景:
- 配置文件管理
- 日志系统
- 数据库连接池
- 缓存管理