单例模式常用写法总结
引言
《创建单例模式的x种方法》在网上已经烂大街了,但这么多方式,会加重我的记忆负担,所以还得做个比较,把知识点浓缩一下,最终列出了三个比较常见的方法(其实是两个,只有静态内部类和枚举没有隐患,双检锁是有隐患的,请看下文中的防反射攻击处理一节)
三种方法比较
如果想要懒加载:
我更推荐静态内部类,因为细节少,写法简单,不容易错。
如果不想要懒加载:
推荐枚举,自带防反射攻击和防序列化攻击,写法简单。
双检锁写法
public class DOUBLE_CHECK_LOCK_TEST {
private volatile static DOUBLE_CHECK_LOCK_TEST Instance = null;
private DOUBLE_CHECK_LOCK_TEST(){}
public static DOUBLE_CHECK_LOCK_TEST getInstance(){
if(Instance == null){
synchronized (DOUBLE_CHECK_LOCK_TEST.class){
if(Instance == null){
Instance = new DOUBLE_CHECK_LOCK_TEST();
}
}
}
return Instance;
}
}
两个关键点,一是volatile防止指令重排,二是synchronized给类上锁
为什么双检锁要加volatile
因为 Instance = new Point(200,1); 这句话不是原子指令,其实有三步:
1.分配对象内存;
2.调用构造器,执行初始化;
3.将对象引用赋值给变量。
其中2和3可能发生指令重排,在并发环境下,线程A可能会先执行1和3,这时恰好切换到线程B,这时线程B就会看到一个不为null,但是没有完成初始化的对象,此时线程B访问这个对象就会发生异常。(详细请看双重检查锁单例模式为什么要用volatile关键字?)
静态内部类写法
public class STATIC_TEST {
private static class STATIC_HOLDER{
private static final STATIC_TEST Instance = new STATIC_TEST();
}
STATIC_TEST(){}
public static final STATIC_TEST getInstance(){
return STATIC_HOLDER.Instance;
}
}
枚举写法
enum ENUM_TEST {
SINGLETON;
public void doSomething() {
System.out.println("doSomething");
}
}
防序列化攻击处理
在单例类中加上readResolve()方法。
private Object readResolve(){
return Instance;
}
防反射攻击处理
在构造函数中加入检查。如果发现已经创建过,则不再创建。以双检锁为例:
private DOUBLE_CHECK_LOCK_TEST(){
if(Instance != null){
System.out.println("发现反射攻击,不许创建新实例!");
throw new IllegalStateException();
}
}
但是在《Java单例---反射攻击单例和解决方法》这篇文章中指出:
如果先通过正常的获取手段获取实例,再进行反射攻击获取实例,此时是能防得住反射攻击的。
但如果反过来,先进行反射攻击获取实例,再通过正常的获取手段获取实例,得到的两个结果不同。也就是说,对于双检锁来说,这种处理依旧不能防止反射攻击,网上的部分博客都是错的。
还有一种尝试解决这种反射攻击的是:在单例里面加标识属性,如果实例化之后,标识改变,在构造器里面判断标识改变就抛异常,和上面这种气势差不多,但是没用的,反射可以把构造器的权限放开,同样可以把属性的权限放开,并且修改属性值,所以这种方式也是不行的。
但是这种处理在静态内部类中,却不会产生上面双检锁出现的"先进行反射攻击获取实例,再通过正常的获取手段获取实例,得到的两个结果不同"的情况。
STATIC_TEST(){
if(STATIC_HOLDER.Instance != null){
System.out.println("发现反射攻击,不许创建新实例!");
throw new IllegalStateException();
}
}
具体原因我还没弄清楚,希望看到的朋友可以解答一下。