0%

浅谈单例模式

单例模式的优点:避免了对象频繁的创建和销毁,节省了性能开销

即:整个过程中只能被new一次

需要考虑五个点:效率(时)、内存(空)、线程安全、反射攻击、反序列化破坏

以下实现方式均是线程安全的

懒汉式

  • 效率很低,因为每次getInstance()都要加锁
  • 第一次调用才初始化,节省内存
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton{
private static Singleton instance;

//私有的构造函数使该类不会被实例化
private Singleton(){}

public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

饿汉式

  • 不加锁,效率高

  • 类加载时就创建实例,浪费内存

1
2
3
4
5
6
7
8
9
public class Singleton{
private static final Singleton INSTANCE=new Singleton();//类加载时就初始化,所以线程安全

private Singleton(){}

public static Singleton getInstance(){
return INSTANCE;
}
}

DCL式★

Double Checked Locking

兼备饿汉式和懒汉式的优点,节省内存高效率

这种方式并不推荐,因为无法防止反序列化破坏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton{
private volatile static Singleton instance;//volatile禁止指令重排

private Singleton(){}

public static Singleton getInstance(){
if(instance==null){//初始化完成后便不会加锁,保证高性能
synchronized(Singleton.class){//加锁实现多个线程串行执行
if(instance==null){//避免重复new
instance=new Singleton();
}
}
}
return instance;
}
}

new Singleton()并不是原子性操作,底层经历了三步

  1. 分配内存空间
  2. 在内存空间初始化对象
  3. 指针指向该空间

不加volatile可能会指令重排,即执行132,若在2没有完成时线程B进入方法,因为instance不指向null,会直接返回instance,但此时instance还未初始化完成

指令重排分为两类,参考

  • 指令集并行重排:由CPU和OS控制

    CPU和内存的交互主要通过CPU总线进行,分为数据总线和地址总线

    控制指令在总线中进行传输时,会有一个指令周期的概念。如果在一个指令周期内,CPU总线发现内存地址不可操作,就会将该指令存入指令序列中,并且尝试执行和前置指令不存在因果关系的后续指令,这个过程称为指令集并行重排

  • 编译器优化重排:由JVM控制

    javac不会做指令重排。这里的编译器优化是指JVM内置的JIT编译器在装载class文件时的执行优化过程

静态内部类式★

该方式也能实现和DCL式一样的效果

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton{
private Singleton(){}

//静态内部类在外部类加载时不会加载,当被调用时才会加载
private static class SingletonHolder{
private static final Singleton INSTANCE=new Singleton();//类加载时就初始化,所以线程安全
}

public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}

枚举式(最佳)★

反射破坏

可以通过改写私有构造方法抛出异常一定程度防止反射破坏👇,如果两次都用反射创建则这种方法仍会失效

1
2
3
4
5
6
7
private Singleton(){
synchronized(Singleton.class){
if(instance!=null){
throw new Exception("Cannot reflectively create enum objects)
}
}
}

反序列化破坏

用户可以自定义writeObjectreadObject方法控制序列化过程👇,进而防止反序列化破坏

1
2
3
4
@Serial
private Object readObject(){
return instance;
}

单元素的枚举类型已经成为实现Singleton的最佳方法 ——《Effective Java》

枚举类从源码层面根除反射攻击

Constructor类的newInstance方法👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public T newInstance(Object ... initargs){
...
return newInstanceWithCaller(initargs, !override, caller);
}

T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller){
...
//Class.getModifiers是返回int的native方法
//所有修饰符的int编码都是只有一位为1,其余全为0,所以可以直接用&判断是否相等

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
...
}

枚举类的序列化机制:

  • 仅仅将枚举对象的name属性输出到序列化结果中
  • 反序列化时通过java.lang.EnumvalueOf方法来根据名字查找枚举对象

同时,编译器不允许自定义序列化,因此禁用了writeObjectreadObject等方法

枚举实现单例模式👇

1
2
3
public enum Singleton{
INSTANCE;
}

反编译后的代码👇

1
2
3
4
5
6
7
8
9
10
11
public final class Singleton extends Enum{
public static final Singleton INSTANCE;
private static final Singleton ENUM$VALUES[];

static{
INSTANCE = new Singleton("INSTANCE", 0);
ENUM$VALUES = (new Singleton[] {
INSTANCE
});
}
}

可以看到枚举类并不是懒加载,但是如果一个单例类除了这个单例之外,没有其他静态变量或静态方法,那么直接用饿汉式是很合适的,因为这种情况下除了获取单例之外通常没有其他会导致这个类被加载的情况存在,类加载本身就已经是一种懒加载了