很久没有写过接地气的东西了,今天随便写一个非常基础的。其实这篇文章也可以叫做《Java单例的破坏与防御方法》,无所谓了。
讲解Java单例实现方式及其原理的文章数不胜数,本文就不再多废话。在实际生产环境中,以下3种方式最常用,先复习一下。看官也可以试试能不能不参考任何资料,将下面的问题都回答正确。
Java单例的三种经典实现
双重检查锁(DCL)
public class DoubleCheckLockSingleton {
private static volatile DoubleCheckLockSingleton instance;
private DoubleCheckLockSingleton() {}
public static DoubleCheckLockSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
public void tellEveryone() {
System.out.println("This is a DoubleCheckLockSingleton " + this.hashCode());
}
}
- volatile关键字在此处起了什么作用?
- 为何要执行两次
instance == null
判断?
静态内部类
public class StaticInnerHolderSingleton {
private static class SingletonHolder {
private static final StaticInnerHolderSingleton INSTANCE = new StaticInnerHolderSingleton();
}
private StaticInnerHolderSingleton() {}
public static StaticInnerHolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void tellEveryone() {
System.out.println("This is a StaticInnerHolderSingleton" + this.hashCode());
}
}
- 这种方式是通过什么机制保证线程安全性与延迟加载的?(注意,这是Java单例的两大要点,必须保证)
枚举
public enum EnumSingleton {
INSTANCE;
public void tellEveryone() {
System.out.println("This is an EnumSingleton " + this.hashCode());
}
}
- Java枚举的本质是?
- 这种方式又是通过什么机制保证线程安全性与延迟加载的?
复习完了。
在Java圣经《Effective Java》中,Joshua Bloch大佬如是说:
A single-element enum type is often the best way to implement a singleton.
为什么说枚举是(一般情况下)最好的Java单例实现呢?他也做出了简单的说明:
It is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks.
大意就是,枚举单例可以有效防御两种破坏单例(即使单例产生多个实例)的行为:反射攻击与序列化攻击(虽然我之前讲过“简单易懂的现代魔法”Unsafe,但它过于邪门歪道了,不算数)。言外之意就是前两种单例方式都会被破坏。那么我们就拿平时最常用的双重检查锁方式开刀来试试看。
如何破坏一个单例
反射攻击
直接上代码:
public class SingletonAttack {
public static void main(String[] args) throws Exception {
reflectionAttack();
}
public static void reflectionAttack() throws Exception {
Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton)constructor.newInstance();
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)constructor.newInstance();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
}
执行结果如下:
This is a DoubleCheckLockSingleton 1368884364
This is a DoubleCheckLockSingleton 401625763
false
这种方法非常简单暴力,通过反射侵入单例类的私有构造方法并强制执行,使之产生多个不同的实例,这样单例就被破坏了。要防御反射攻击,只能在单例构造方法中检测instance是否为null,如果已不为null,就抛出异常。显然双重检查锁实现无法做这种检查,静态内部类实现则是可以的。
注意,不能在单例类中添加类初始化的标记位或计数值(比如boolean flag
、int count
)来防御此类攻击,因为通过反射仍然可以随意修改它们的值。
序列化攻击
这种攻击方式只对实现了Serializable接口的单例有效,但偏偏有些单例就是必须序列化的。现在假设DoubleCheckLockSingleton类已经实现了该接口,上代码:
public class SingletonAttack {
public static void main(String[] args) throws Exception {
serializationAttack();
}
public static void serializationAttack() throws Exception {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
outputStream.writeObject(s1);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)inputStream.readObject();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
}
执行结果如下:
This is a DoubleCheckLockSingleton 777874839
This is a DoubleCheckLockSingleton 254413710
false
为什么会发生这种事?长话短说,在ObjectInputStream.readObject()方法执行时,其内部方法readOrdinaryObject()中有这样一句话:
obj = desc.isInstantiable() ? desc.newInstance() : null;
其中desc是类描述符。也就是说,如果一个实现了Serializable/Externalizable接口的类可以在运行时实例化,那么就调用newInstance()方法,使用其默认构造方法反射创建新的对象实例,自然也就破坏了单例性。要防御序列化攻击,就得将instance声明为transient,并且在单例中加入以下语句:
private Object readResolve() {
return instance;
}
这是因为在上述readOrdinaryObject()方法中,会通过卫语句desc.hasReadResolveMethod()
检查类中是否存在名为readResolve()的方法,如果有,就执行desc.invokeReadResolve(obj)
调用该方法。readResolve()会用自定义的反序列化方法覆盖默认实现,因此强制它返回instance本身,就可以防止产生新的实例。
枚举单例的防御机制
对反射的防御
我们直接将上述reflectionAttack()方法中的类名改成EnumSingleton并执行,会发现报如下异常:
Exception in thread "main" java.lang.NoSuchMethodException: me.lmagics.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at me.lmagics.singleton.SingletonAttack.reflectionAttack(SingletonAttack.java:35)
at me.lmagics.singleton.SingletonAttack.main(SingletonAttack.java:19)
这是因为所有Java枚举都隐式继承自Enum抽象类,而Enum抽象类根本没有无参构造方法,只有如下一个构造方法:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
那么我们就改成获取这个有参构造方法,即:
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
结果还是会抛出异常:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at me.lmagics.singleton.SingletonAttack.reflectionAttack(SingletonAttack.java:38)
at me.lmagics.singleton.SingletonAttack.main(SingletonAttack.java:19)
来到Constructor.newInstance()方法中,有如下语句:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
可见,JDK反射机制内部完全禁止了用反射创建枚举实例的可能性。
对序列化的防御
如果将serializationAttack()方法中的攻击目标换成EnumSingleton,那么我们就会发现s1和s2实际上是同一个实例,最终会打印出true。这是因为ObjectInputStream类中,对枚举类型有一个专门的readEnum()方法来处理,其简要流程如下:
- 通过类描述符取得枚举单例的类型EnumSingleton;
- 取得枚举单例中的枚举值的名字(这里是INSTANCE);
- 调用Enum.valueOf()方法,根据枚举类型和枚举值的名字,获得最终的单例。
这种处理方法与readResolve()方法大同小异,都是以绕过反射直接获取单例为目标。不同的是,枚举对序列化的防御仍然是JDK内部实现的。
综上所述,枚举单例确实是目前最好的单例实现了。
网友评论