单例模式的优点:避免了对象频繁的创建和销毁,节省了性能开销
即:整个过程中只能被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; private Singleton(){} public static Singleton getInstance(){ if(instance==null){ synchronized(Singleton.class){ if(instance==null){ instance=new Singleton(); } } } return instance; } }
|
new Singleton()并不是原子性操作,底层经历了三步
- 分配内存空间
- 在内存空间初始化对象
- 指针指向该空间
不加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) } } }
|
反序列化破坏
用户可以自定义writeObject和readObject方法控制序列化过程👇,进而防止反序列化破坏
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){ ... if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ... }
|
枚举类的序列化机制:
- 仅仅将枚举对象的name属性输出到序列化结果中
- 反序列化时通过
java.lang.Enum的valueOf方法来根据名字查找枚举对象
同时,编译器不允许自定义序列化,因此禁用了writeObject、readObject等方法
枚举实现单例模式👇
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 }); } }
|
可以看到枚举类并不是懒加载,但是如果一个单例类除了这个单例之外,没有其他静态变量或静态方法,那么直接用饿汉式是很合适的,因为这种情况下除了获取单例之外通常没有其他会导致这个类被加载的情况存在,类加载本身就已经是一种懒加载了