单例模式定义与用途
单例模式是一种常见的软件设计模式,其目的是确保一个类在任何情况下只有一个实例,并提供一个全局访问点供外部获取这个唯一的实例。
这种模式特别适用于那些具有全局状态的场合,如配置管理器、线程池、缓存、对话管理等。
Java 实现单例模式的方法
1. 懒汉式
命名来源: 懒汉式的名称来自于他"懒惰"的特性。与饿汉式不同,懒汉式单例类在被加载时并不立即创建实例,而是等到第一次被使用时才创建。这就像一个"懒汉",总是推迟行动,直到不得不行动的时候。
实现特定: 懒汉式的优点是它支持延迟加载,既仅在需要时才创建实例,这样可以节省资源。然而,这种方式需要处理线程安全问题,因为在多线程环境下,如果多个线程同时访问这个类的实例化方法,就可能创建出多个实例,这违背了单例模式的初衷。
线程不安全
实现代码:
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingletion() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
上面的写法在多线程环境下不安全。
线程安全
实现代码:
java
public class SafeLazySingleton {
private static SafeLazySingleton instance;
private SafeLazySingleton() {}
public static synchronized SafeLazySingleton getInstance() {
if (instance == null) {
instance = new SafeLazySingleton();
}
return instance;
}
}
2. 饿汉式
命名来源: 这种方式被称为"饿汉式"是因为它非常"饿",既在类被加载到内存后就立即实例化单例对象,无论你最终是否需要使用这个实例。这种策略类似于一个"饿汉",他总是急切地想立即 吃东西,不管是否真正饿了。
实现特点: 饿汉实现简单,因为它在类加载时就完成了实例的初始化,因此也避免了线程同步问题。但它的缺点在于,如果该单例类的实例化过程中需要加载大量资源或执行长时间的初始化,而应用程序又可能并不需要这个实例,那么这种预先加载的方式会导致资源的浪费。
实现代码
java
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
上面的代码线程安全,但是在类加载时就会创建实例,不是懒加载模式。
3. 双重检查锁定(DCL)
"双重检查锁定"(Double-Checked Locking, 简称 DCL)这个名称描述了这种实现模式的关键特性:在创建对象时进行两次条件检查(是否实例已被创建),这两次检查都是为了确保只有一个实例被创建。这个模式的第一次检查发生在同步代码块之外,如果实例已经被创建,就直接返回该实例,避免了进入同步代码块;只有当实力尚未创建时,才进入同步代码块。在同步代码块内部,实现再次检查以确保在等待获取锁的期间实例没有被创建。这种方法确保了单例的线程安全,同时减少了同步带来的性能损耗。
示例代码
java
public class DCLSingleton {
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) { // First check (no locking)
synchronized (DCLSingleton.class) {
if (instance == null) { // Second check (with locking)
instance = new DCLSingleton();
}
}
}
return instance;
}
}
为何 DCLSingleton 需要被 volatile 修饰
在 Java 中,volatile 关键字的主要作用是确保变量的修改对所有线程立即可见,并防止编译器对带有该关键字的代码进行重排序。在双重检查锁定模式中,volatile 的使用至关重要,原因包括:
- 可见性:使用 volatile 修饰 instance 确保当一个线程修改了 instance 的值(既初始化了单例对象)后,这个修改对于访问该变量的其他线程立即可见。这样,一旦 instance 被初始化,所有线程都能看到实际的 instance 值,避免了返回一个尚未完全构造好的对象。
- 防止指令重排序:在 Java 的内存模型中,没有 volatile 修饰的变量可能会被编译器或处理器重排序,导致初始化 Singleton 实例的构造函数调用发生在分配内存的操作之后但是赋值之前。这意味着,一个线程可能看到一个非 null 的 instance 引用,但那个对象可能还没有被完全初始化。使用 volatile 可以防止这种情况,它确保在对象的引用被设置之前,对象的构造函数已经被完全执行完毕。
没有使用 volatile 时的问题
- 实例化对象的步骤 :
步骤1:分配对象内存空间
步骤2:初始化对象
步骤3:设置 instance 指向刚分配的内存地址(此时 instance 不再是 null)- 但是,由于Java内存模型的允许重排序,步骤2和步骤3的顺序可能会交换 。这样,其他线程可能会看到一个非null的 instance,但它可能是一个未完全构造的对象。
使用 volatile 的效果
- volatile 关键字防止了指令重排序,确保步骤2(初始化对象)在步骤3(instance 指向内存空间)之前完成。这样,所有看到非null instance 的线程都会看到一个已经初始化的对象。
步骤举例说明
假设有两个线程 A 和 B,它们同时运行 getInstance() 方法:
- 线程A:进入 getInstance() 方法,发现 instance 是 null,进入同步块,再次检查 instance 仍为 null。它开始执行 new Singleton(),由于没有使用 volatile,指令重排序可能发生,导致 instance 先被设置为非 null。线程 A 被中断,未完成初始化。
- 线程B :此时进入 getInstance() 方法,看到 instance 已经不是 null(尽管实例尚未完全初始化),返回这个未完全初始化的对象
这种情况可能导致程序行为出现异常,因此,使用 volatile 是防止此类问题发生的重要机制。
4. 枚举单例模式
枚举单例模式是使用 Java 的枚举类型来实现单例模式的方法。这种方式不仅能自动支持序列化机制而且通过 JVM 从根本上保证反射攻击的安全问题。
实现细节
- 枚举类的加载:Java 的枚举类的加载和初始化由 JVM 控制。当枚举类被加载时,其定义的枚举值(实例)在类加载时一次性实例化。
- 线程安全性:由于枚举实例创建是在类加载时完成的,由 JVM 保证其线程安全性。因此,使用枚举方式实现的单例模式无需担心多线程并发和同步问题。
- *防止通过反射或序列化破坏单例:枚举类默认禁止通过反射创建对象,同时每个枚举实例都是通过枚举类自动维护的静态实例来管理,因此反序列化时不会重新创建实例。
示例代码
java
public enum Singleton {
INSTANCE;
public void someServiceMethod() {
System.out.println("Performing a service");
}
}
5. 静态内部类单例模式
静态内部类单例模式是通过内部类的特性和类的加载机制来确保单例只被创建一次,同时实现延迟加载和高效性。
实现细节
- 静态内部类 :在外部类加载的时候,并不会立即加载内部类,内部类不会在外部类加载时立即加载,而是在需要实例化时,调用
getInstance()
方法,从而导致SingletonHolder.INSTANCE
被引用,这才会导致内部类加载和初始化其静态成员。 - 类初始化的线程安全性:类的初始化是由 JVM 来控制的,确保一个类的静态初始化器在多线程环境中被正确地加载、链接和初始化。这就像是由 JVM 提供的一个同步锁。
- 延迟加载和性能:这种方式实现了延迟加载和高效性。单例实例仅在第一次被使用时创建,并且创建过程由 JVM 的类初始化机制保证其线程安全性。
代码示例
java
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
总结:
Java实现单例模式主要有以下几种方式:
- 饿汉式(静态常量):在类加载时就完成了实例化,避免了线程同步问题。
- 懒汉式(线程安全,同步方法):在第一次调用时实例化对象,实现了懒加载。
- 双重检查锁定(DCL,double-checked locking):在懒汉式的基础上,通过双重检查锁定机制保证了线程安全和懒加载。
- 静态内部类(推荐):利用静态内部类的特性,实现了懒加载且线程安全。
- 枚举(推荐):通过枚举类型实现单例,简洁且线程安全。