单例模式的优点:避免了对象频繁的创建和销毁,节省了性能开销
即:整个过程中只能被new一次
需要考虑五个点:效率(时)、内存(空)、线程安全、反射攻击、反序列化破坏
以下实现方式均是线程安全的
懒汉式
- 效率很低,因为每次
getInstance()
都要加锁 - 第一次调用才初始化,节省内存
1 | public class Singleton{ |
饿汉式
不加锁,效率高
类加载时就创建实例,浪费内存
1 | public class Singleton{ |
DCL式★
Double Checked Locking
兼备饿汉式和懒汉式的优点,节省内存且高效率
这种方式并不推荐,因为无法防止反序列化破坏
1 | public class Singleton{ |
new Singleton()
并不是原子性操作,底层经历了三步
- 分配内存空间
- 在内存空间初始化对象
- 指针指向该空间
不加volatile
可能会指令重排,即执行132,若在2没有完成时线程B进入方法,因为instance不指向null,会直接返回instance,但此时instance还未初始化完成
指令重排分为两类,参考:
指令集并行重排:由CPU和OS控制
CPU和内存的交互主要通过CPU总线进行,分为数据总线和地址总线
控制指令在总线中进行传输时,会有一个指令周期的概念。如果在一个指令周期内,CPU总线发现内存地址不可操作,就会将该指令存入指令序列中,并且尝试执行和前置指令不存在因果关系的后续指令,这个过程称为指令集并行重排
编译器优化重排:由JVM控制
javac不会做指令重排。这里的编译器优化是指JVM内置的JIT编译器在装载class文件时的执行优化过程
静态内部类式★
该方式也能实现和DCL式一样的效果
1 | public class Singleton{ |
枚举式(最佳)★
反射破坏
可以通过改写私有构造方法抛出异常一定程度防止反射破坏👇,如果两次都用反射创建则这种方法仍会失效
1 | private Singleton(){ |
反序列化破坏
用户可以自定义writeObject
和readObject
方法控制序列化过程👇,进而防止反序列化破坏
1 |
|
单元素的枚举类型已经成为实现Singleton的最佳方法 ——《Effective Java》
枚举类从源码层面根除了反射攻击
Constructor
类的newInstance
方法👇
1 | public T newInstance(Object ... initargs){ |
枚举类的序列化机制:
- 仅仅将枚举对象的name属性输出到序列化结果中
- 反序列化时通过
java.lang.Enum
的valueOf
方法来根据名字查找枚举对象
同时,编译器不允许自定义序列化,因此禁用了writeObject
、readObject
等方法
枚举实现单例模式👇
1 | public enum Singleton{ |
反编译后的代码👇
1 | public final class Singleton extends Enum{ |
可以看到枚举类并不是懒加载,但是如果一个单例类除了这个单例之外,没有其他静态变量或静态方法,那么直接用饿汉式是很合适的,因为这种情况下除了获取单例之外通常没有其他会导致这个类被加载的情况存在,类加载本身就已经是一种懒加载了