单例模式的各种写法
2024-09-30 08:00:49 # Technical # JavaBase

单例可以确保一个类只有一个实例,同时提供对此实例的全局访问

单例模式的好处:

  • 避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间
  • 避免由于操作多个实例导致的逻辑错误
  • 起到了全局统一管理控制的作用

饿汉模式

1
2
3
4
5
6
7
8
9
class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

private EagerInitializedSingleton() {}

public static EagerInitializedSingleton getInstance() {
return instance;
}
}

优点:

在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题

缺点:

单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费

适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到就不合适了

懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LazyInitializationSingleton {

// 注意用 volatile 来保证可见性和有序性
private static volatile LazyInitializationSingleton INSTANCE;

private LazyInitializationSingleton() {}

public static LazyInitializationSingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazyInitializationSingleton.class) {
if (INSTANCE == null) {
INSTANCE = new LazyInitializationSingleton();
}
}
}
return INSTANCE;
}
}

优点:

需要的时候才去初始化对象

缺点:

要注意线程安全问题

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
class StaticInnerSingleton {

private StaticInnerSingleton() {}

public static StaticInnerSingleton getInstance() {
return InnerSingleton.INSTANCE;
}

public static class InnerSingleton {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
}

通过 类加载机制 保证只会创建一个实例

并且只要应用中不适用内部类,JVM 就不会去加载这个单例类,也就不会创建单例对象

这种方式可以很简单的实现懒汉模式

反射打破单例

上面的实现方式都可以通过反射来打破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ReflectionSingletonTest {

public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException {
EagerInitializedSingleton instance1 = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instance2 = null;
Constructor<?>[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
constructor.setAccessible(true);
instance2 = (EagerInitializedSingleton) constructor.newInstance();
break;
}
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

序列化打破单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ReflectionSingletonTest {

public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException {
EagerInitializedSingleton instance1 = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instance2 = null;
Constructor<?>[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
constructor.setAccessible(true);
instance2 = (EagerInitializedSingleton) constructor.newInstance();
break;
}
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

反序列化的时候会生成一个新的对象

解决方法是提供一个 readResolve 方法

1
2
3
protected Object readResolve() {
return getInstance();
}

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class EnumSingleton {

private EnumSingleton() {}

public static EnumSingleton getInstance() {
return instanceEnum.INSTANCE.instance;
}

private enum instanceEnum {
INSTANCE;

final EnumSingleton instance;

instanceEnum() {
instance = new EnumSingleton();
}
}
}

通过枚举的方式来实现单例模式一直被誉为最佳方法

而且 反射和反序列化都无法打破枚举方式的单例

当反射通过 newInstance 创建对象时,会检查该类是否被 enum 修饰,如果是则抛出异常

序列化时,Java 仅仅是将枚举对象的 name 对象序列化,反序列化时这是通过 java.lang.Enum 的 valueOf 方法根据名字来查找枚举对象