1561 字
8 分钟
线程安全单例

1 解决反序列化导致的单例破坏现象#

这里的单例问题是,如果对一个可序列化对象进行反序列化,会创建一个新的对象,这就违背了我们想要全局单例的目标。因此要重写readResolve方法。

package org.example.sigletons;
import java.io.Serializable;
public class Singleton1 implements Serializable {
private Singleton1(){}
private static final Singleton1 INSTANCE = new Singleton1();
public Singleton1 getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}

2 使用枚举实现单例模式#

package org.example.sigletons;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public enum Singleton2 {
INSTANCE;
private final Properties properties;
private Singleton2() {
properties = new Properties();
String configFile = "application.properties";
System.out.println("ConfigurationManager: Initializing and loading " + configFile);
try(InputStream inputStream = Singleton2.class.getClassLoader().getResourceAsStream(configFile)){
if(inputStream == null){
System.out.println("ConfigurationManager: Sorry, unable to find " + configFile);
// 在实际应用中,这里可能抛出异常或有更复杂的错误处理
return;
}
properties.load(inputStream);
System.out.println("ConfigurationManager: Configuration loaded successfully.");
}catch (IOException e) {
e.printStackTrace();
}
}
public String getProperty(String key) {
return properties.getProperty(key);
}
public String getProperty(String key,String defaultValue) {
return properties.getProperty(key, defaultValue);
}
// 可以添加其他需要的方法,比如重新加载配置等(需要考虑线程安全)
public void listProperties() {
properties.forEach((key, value) -> System.out.println(key + "=" + value));
}
}
class TestSingleton2 {
public static void main(String[] args) {
Singleton2 singleton2 = Singleton2.INSTANCE;
singleton2.listProperties();
}
}

💎 枚举单例:全面解答这些问题!📜✨

枚举单例是一种非常推荐的单例实现方式,因为它不仅简单、易用,还天然地具备线程安全和防止反序列化、反射破坏单例的能力。接下来,我们重点针对 枚举单例 来回答这些问题!


问题 1:枚举单例是如何限制实例个数的?#

枚举单例通过枚举的机制天然地保证:

  1. 枚举类的每一个枚举实例(如单例对象)都在 类加载阶段 就完成初始化,并且整个应用程序中只有一个实例。
  2. 枚举类型底层由 JVM 的实现机制保证,它不像普通类那样允许通过反射或 new 额外创建实例。

示例:#

public enum SingletonEnum {
INSTANCE; // 枚举单例实例
public void doSomething() {
System.out.println("Doing something...");
}
}

使用方式:#

即便通过 SingletonEnum.INSTANCE 多次获取,得到的始终是同一个实例。

SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
System.out.println(instance1 == instance2); // 输出:true

问题 2:枚举单例在创建时是否有并发问题?#

枚举单例天然线程安全,因为:

  1. 枚举类型的初始化由 JVM 保证,是在类加载时完成的。
  2. 类加载过程是线程安全的,JVM 使用了类加载的同步机制,保证枚举单例的初始化不会因多线程而发生竞争。

举例:#

即使多个线程同时调用 SingletonEnum.INSTANCE,它们都会得到在类加载阶段构造好的唯一对象,无需额外同步。


问题 3:枚举单例能否被反射破坏单例?#

不会! 枚举类型的结构特殊,无法被反射破坏单例。这是因为:

  1. 枚举的构造器是私有的,并且其底层会检测反射调用。
  2. 如果尝试通过反射显式调用枚举类的构造器,JVM 会抛出 IllegalArgumentException

验证代码:#

import java.lang.reflect.Constructor;
public class EnumReflectionTest {
public static void main(String[] args) {
try {
// 获取枚举的构造器
Constructor<SingletonEnum> constructor = SingletonEnum.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonEnum instance = constructor.newInstance(); // 反射创建枚举对象
} catch (Exception e) {
e.printStackTrace(); // 会抛出 IllegalArgumentException
}
}
}

运行结果:#

java.lang.IllegalArgumentException: Cannot reflectively create enum objects

问题 4:枚举单例能否被反序列化破坏单例?#

枚举单例天然具备防止反序列化破坏的特性,原因是:

  1. Enum 类型的序列化机制是由 JVM 内部实现的,不走普通的对象序列化流程。
  2. 反序列化枚举对象时,JVM 会直接返回枚举类中的现有实例,而不是从序列化流中创建新对象。

验证代码:#

import java.io.*;
public class EnumSerializationTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SingletonEnum instance1 = SingletonEnum.INSTANCE;
// 序列化枚举对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("enum_singleton.obj"));
oos.writeObject(instance1);
oos.close();
// 反序列化枚举对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("enum_singleton.obj"));
SingletonEnum instance2 = (SingletonEnum) ois.readObject();
// 判断是否破坏单例
System.out.println(instance1 == instance2); // 输出:true,单例没有破坏
}
}

问题 5:枚举单例属于懒汉式还是饿汉式?#

枚举单例本质上属于饿汉式单例。它的特点是在类加载阶段完成初始化

  • 枚举的实例在类加载时就被创建并初始化。
  • 即使程序中从未访问过 SingletonEnum.INSTANCE,枚举实例依然会被加载。

优点:#

  • 线程安全,无需为单例初始化额外编写同步代码。
  • 实现简洁,JVM 自动保证。

缺点:#

  • 如果枚举实例较多,并且包含较大的初始化逻辑,会导致类加载阶段性能开销增加。

问题 6:枚举单例如果希望加入一些初始化逻辑,该如何做?#

可以通过添加枚举的构造方法和静态方法来实现初始化逻辑。枚举的构造方法是私有的,可以用来在实例创建时执行初始化。

修改代码:#

public enum SingletonEnum {
INSTANCE; // 枚举单例实例
private String configuration;
// 枚举的构造方法
SingletonEnum() {
// 初始化逻辑
configuration = "System Configuration Loaded";
}
public String getConfiguration() {
return configuration;
}
}

测试:#

public class TestEnumInitialization {
public static void main(String[] args) {
SingletonEnum instance = SingletonEnum.INSTANCE;
System.out.println(instance.getConfiguration()); // 输出:System Configuration Loaded
}
}

分析:#

  • 枚举类型的构造器会在类加载时调用,且只调用一次。
  • 可用枚举构造器实现单例实例的初始化逻辑。

总结#

为何枚举单例完美适合单例模式?

  • 它是天生线程安全的,JVM 保障了枚举实例的唯一性。
  • 枚举实例不能通过反射或序列化破坏。
  • 枚举的初始化流程天然符合饿汉式单例的特点。

3 Double Check#

https://meowrain.cn/archives/volatile-shi-xian-dan-li-mo-shi-de-shuang-zhong-suo

package cn.meowrain;
public class DoubleSingleton {
private static volatile DoubleSingleton INSTANCE = null;
public static DoubleSingleton getInstance() {
if(INSTANCE != null) {
return INSTANCE;
}
synchronized (DoubleSingleton.class){
if(INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new DoubleSingleton();
return INSTANCE;
}
}
}

4 静态内部类懒汉式创建线程安全单例#

package cn.meowrain;
public class Singleton2 {
private Singleton2(){}
// 问题1: 属于懒汉式还是饿汉式
private static class LazyLoader{
static final Singleton2 INSTANCE = new Singleton2();
}
// 在创建的时候是否有并发问题
public static Singleton2 getInstance() {
return LazyLoader.INSTANCE;
}
}