本文主要分析单例模式在多线程、多处理器、反射、序列化等方面的安全问题,以及在 JDK和开发框架中的应用。
ps:原本查资料看代码写了三个小时,结果笔记本抽风重启,全没了……没了……了
概述
- 私有的构造方法
- 指向自己唯一实例的私有对象
- 返回唯一实例的公共方法以供外界访问
饿汉模式
在类加载时就初始化实例对象,线程安全
class Single{
private static Single instance = new Single();
private Single(){}
public static Single getInstence(){
return instance;
}
}
问题:浪费内存
改进办法:懒汉模式,即惰性初始化
懒汉模式
使用时才初始化实例对象
class Single{
private static Single instance = null;
private Single(){}
public static Single getInstance(){
if(instance == null){
return new Single();
}
return instance;
}
}
问题: 非线程安全
instance
被多次实例化- 获取未完成初始化的对象
改进办法:对方法加同步锁
class Single{
private static Single instance = null;
private Single(){}
public static synchronized Single getInstance(){
if(instance == null){
return new Single();
}
return instance;
}
}
问题: synchronized
是重量级锁,效率低
改进办法:对代码块加同步锁
class Single{
private static Single instance = null;
private Single(){}
public static Single getInstance(){
if(instance == null){
synchronized(Single.class){
instance = new Single();
}
}
return instance;
}
}
问题: 线程不安全,同不加锁
改进办法:二次检查
class Single{
private static Single instance = null;
private Single(){}
public static Single getInstence(){
if(instance == null){
synchronized(Single.class){
if(instance == null){
instance = new Single();
}
}
}
return instance;
}
}
问题:
- 处理器指令乱序执行
- 编译器指令重排序
instance = new Single();
正常顺序应是:、初始化、分配内存、赋值
执行过程可能是:分配内存、赋值、初始化
改进办法: instance
加volatile
关键字修饰
volatile保证多线程可见性,提示线程每次从共享内存中读取变量,而不是从私有内存中读取
相当于插入LoadStore内存屏障,禁止指令重排序
到此为止,安全的单例模式基本实现。
为什么是基本呢?还有如下安全问题。
其他安全问题
反射
调用私有的构造函数
解决办法:设置变量标识,阻止多次访问
序列化
解决办法:反序列化时,指定对象实例(会自动调用该方法)
private Object readResolve() {
return singleton;
}
克隆
不实现clonable接口即可
更好的实现
静态内部类
- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确的加锁、同步 - JVM类加载机制:实现惰性初始化
只有new、getstatic、putstatic、invokestatic指令、反射、主类等才会触发初始化
也就是说,只有访问静态字段INSTANCE
才会触发
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
枚举类型
public enum Singleton {
INSTANCE;
private Singleton() {}
}
功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化
为什么呢?
枚举变量在编译时被声明为static,保证正确初始化
Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的
valueOf() 方法来根据名字查找枚举对象。
JDK中单例模式
Runtime
NumberFormat
待续
Spring中单例模式
Spring依赖注入Bean实例默认是单例的
DefaultSingletonBeanRegistry.java
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}