单例模式及 Java 实现
概述
单例模式是一种创建型设计模式,它保证一个类仅有一个实例,并提供一个全局访问点。
实现
单例模式的实现方式
- 私有构造函数,禁止外部创建对象。
- 定义一个静态私有变量,保存类的唯一实例。
- 提供一个静态公共方法,获取类的唯一实例。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
// other methods...
}
更多的实现方式?
上述的实现模式属于饿汉式实现。之所以叫饿汉式,是因为在类加载时实例就已经被创建。
这种做法的代价就是,如果没有用到该实例,则浪费资源。
事实上,单例模式创建的实例往往体积很大,采用饿汉式实现,类加载时需要耗费大量时间,占用内存资源。
于是有人提出了懒汉式实现等更多的实现方式。
懒汉式实现
懒汉式实现是指,在需要用到实例时,才去创建实例。
这显得很“懒”,但当创建的实例很大时,懒汉式实现可以节省内存资源、提高程序运行效率。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 1
instance = new Singleton(); // 2
}
return instance;
}
// other methods...
}
这样实现的好处是,类加载时并不会创建实例,只有在第一次调用 getInstance() 方法时,才会创建实例。
然而,这种实现方式只有在单线程的情况下才能保证实例的唯一性,多线程环境下单例有可能被破坏。我们来看如下分析。
假设现在有两个线程 A 和 B,都调用 getInstance() 方法。
线程 A 率先进入方法体并执行第 1 行代码,此时实例数量为 0 因此判空为 true 进入第 2 行代码。
此时线程 B 也进入方法体并执行第 1 行代码,此时实例数量为 0 因此判空为 true 进入第 2 行代码。
线程 A 执行第 2 行代码,创建实例并将实例变量 instance 指向该实例,并返回该实例,实例数量为 1。
线程 B 执行第 2 行代码,创建实例并将实例变量 instance 指向该实例,并返回该实例,实例数量为 2。
此时,线程 A 和线程 B 都认为自己创建的实例是唯一的,但实际上 A 线程对实例的修改不会影响到 B 线程的实例。
同步方法懒汉式实现
上述分析表明,多线程情况下会发生线程安全问题,导致实例唯一性被破坏。
为了解决这个问题,我们可以采用同步方法懒汉式的实现方式。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// other methods...
}
这样一来,getInstance() 方法就变成了原子操作,线程安全问题解决了。
双重检查锁定懒汉式实现
双重检查锁定是一种优化方案,它在同步方法懒汉式实现的基础上,进一步优化了线程安全问题。
观察上述代码我们发现,当实例已经被唯一创建后,getInstance() 就不需要继续同步了,多个线程就可以同时进入方法体获取实例。
因此,我们可以将 getInstance() 方法修改为双重检查锁定方式。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
// only creations are synchronized
createInstance();
}
return instance;
}
private synchronized static void createInstance() {
if (instance == null) {
instance = new Singleton();
}
}
// other methods...
}
这里说明一下 volatile 关键字的作用。
- 如果一个变量被 volatile 修饰,当多个线程访问该变量时能保证该变量的可见性,即一个线程修改了变量的值,其他线程能立即得知。
- 如果一个变量被 volatile 修饰,JVM 将不会进行指令重排序,该变量将不会以半初始化的状态被引用到栈内存中。
在 createInstance() 方法中,我们将 instance 变量声明为 volatile 以保证单例对象在被赋值前被完整初始化。
使用静态内部类实现
静态内部类也属于懒汉式实现。不同的是,这种方法并不需要在 getInstance 方法体中使用双重检查锁定创建对象,而是巧妙地让对象伴随着静态内部类的加载而被创建。这个静态内部类会被延迟加载,且即便多线程并发调用,类只会加载一次。
我们来看具体的代码。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
if (instance == null) {
instance = SingletonHolder.INSTANCE;
}
return instance;
}
// other methods...
}
当 Singleton 类被加载时,如果 getInstance 未被调用,静态内部类 SingletonHolder 是不会被加载的。单例不会被创建。
当 getInstance 被调用时,静态内部类 SingletonHolder 被加载,其静态字段 Singleton INSTANCE 会被创建。
由于静态内部类只会被加载一次,所以对象只会被创建一次,保证了并发场景下的线程安全。
枚举实现
反射和反序列化对单例模式的破坏:
-
反射可以绕过单例类的私有构造函数,从而创建出多个实例。
使用下面的代码就可以通过反射创建出多个实例:
// 获取 Singleton 类的构造函数 Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); // 设置构造函数为可访问 constructor.setAccessible(true); // 通过反射调用私有构造函数创建新的实例 Singleton s1 = constructor.newInstance(); Singleton s2 = constructor.newInstance(); Singleton s3 = constructor.newInstance(); -
同样地,如果单例类实现了
Serializable接口,但没有正确处理反序列化逻辑,就可能导致多个实例被创建。运行下面的代码,得到输出结果。
// 获取单例实例 Singleton instance1 = Singleton.getInstance(); System.out.println("Instance 1: " + instance1); // 序列化单例实例 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"))) { oos.writeObject(instance1); } catch (IOException e) { e.printStackTrace(); } // 反序列化单例实例 Singleton instance2; try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"))) { instance2 = (Singleton) ois.readObject(); System.out.println("Instance 2: " + instance2); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } // 比较两个实例 System.out.println("Instance 1 == Instance 2: " + (instance1 == instance2));输出结果将类似于:
Singleton instance created. Instance 1: Singleton@6d06d69c Instance 2: Singleton@7852e922 Instance 1 == Instance 2: false
在单例模式的实现方式中,枚举实现是一种非常简洁且线程安全的实现方式。它利用了 Java 枚举的特性,天然地保证了单例的唯一性。
枚举实现单例模式是基于 Java 枚举的特性。Java 枚举在运行时只会加载一次,且枚举实例的创建是线程安全的。因此,使用枚举实现单例模式可以避免线程安全问题,同时代码也非常简洁:
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("This is a singleton method.");
}
}
- Java 枚举的实例化是线程安全的,不需要额外的同步机制。
- 代码非常简洁,不需要手动管理实例的创建和访问。
- 枚举的单例实现可以防止反射攻击,因为
Enum类会抛出异常来阻止通过反射创建额外的实例。 - 枚举的单例实现可以防止通过序列化和反序列化创建多个实例。
虽然枚举实现非常强大,但它也有一些局限性:
- 枚举实现的单例模式无法继承其他类(因为枚举本身继承了
java.lang.Enum)。 - 枚举实例在类加载时就会被初始化,无法像懒汉式那样延迟初始化。

4388

被折叠的 条评论
为什么被折叠?



