Java - Java单例的破坏与防御方法
1. Java单例实现的三种经典方式
1.1 双重检查锁实现
package create_singal;
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 tell(){
System.out.println("This is a DoubleCheckLockSingleton " + this.hashCode());
}
}
- 进行了两次null判断,这样可以极大地提升并发度,进而提升性能。
单例模式下new情况很少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查,就可以减少绝大多数的加锁操作,也就提高了执行效率。 - 【volatile关键字作用】:
- 其一:可见性(visibility):可见性指在一个线程中对该变量的修改会马上由工作内存写回主内存,所以其他线程就会马上读取到已修改的值避免出现脏读的现象,关于工作内存和主内存可简单理解为高速缓存和主存,注意工作内存是线程独享的,主存是线程共享的。
- 其二:禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令顺序与源代码相同,这在单线程并没什么问题,但在多线程中,就可能出现严重问题,volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在java1.5之后才实现,之前版本仍是不安全的,即使使用了volatile关键字。
1.2 静态内部类实现
package create_singal;
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单例的两大要点,必须保证
延迟加载:内部类只有在被使用的时候才会加载,所以这就实现了延迟加载
线程安全:由于对象instance是静态的,所以当内部类加载的时候它也被创建,并且被static修饰,所以该成员变量就是属于内部类的,不是属于对象的,被所有对象共享,以此保证只有一个实例化对象
1.3 枚举方式实现
package create_singal;
public enum EnumSingleton {
INSTANCE;
public void tellEveryone() {
System.out.println("This is an EnumSingleton " + this.hashCode());
}
}
说明:
枚举类实现单例模式可以使用SingletonEnum.INSTANCE进行访问实例化对象,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。
枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
T result = enumType.enumConstantDirectory().get(name);
//实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
enumConstantDirectory方法源码:
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
//getEnumConstantsShared最终通过反射调用枚举类的values方法
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
//map存放了当前enum类的所有枚举实例变量,以name为key值
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;
从上面两段源码可以看出,枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。
【ava枚举的本质是?】
枚举本质上是一个类,所有的自定义枚举都继承了Enum类
【这种方式又是通过什么机制保证线程安全性与延迟加载的?】
枚举单例可以有效防御两种破坏单例(即使单例产生多个实例)的行为:反射攻击与序列化攻击
2. 破坏单例模式
2.1 反射攻击
package attack_singal;
import create_singal.DoubleCheckLockSingleton;
import java.lang.reflect.Constructor;
public class SingletonAttack {
public static void main(String[] args) throws Exception {
reflectionAttack();
}
/*
* 破坏单例模式 - 反射攻击
* */
private static void reflectionAttack() throws Exception {
//获取DoubleCheckLockSingleton类中的无参静态构造方法
Constructor<DoubleCheckLockSingleton> constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
//取消权限检测机制
constructor.setAccessible(true);
//创建两个单例对象
DoubleCheckLockSingleton d1 = constructor.newInstance();
DoubleCheckLockSingleton d2 = constructor.newInstance();
d1.tell();
d2.tell();
System.out.println(d1==d2);
}
}
执行结果:
This is a DoubleCheckLockSingleton 356573597
This is a DoubleCheckLockSingleton 1735600054
false
- 这种方法非常简单暴力,通过反射侵入单例类的私有构造方法并强制执行,使之产生多个不同的实例,这样单例就被破坏了。要防御反射攻击,只能在单例构造方法中检测instance是否为null,如果不为null,就抛出异常。显然双重检查锁实现无法做这种检查,静态内部类实现则是可以的。
原因:
- 双重检查锁中的实例化时:直接new一个对象,该结果是属于对象的,每次通过反射创建对象前判断都会得到对象为null,那么就会创建新对象,所以不会有效果而静态内部类实现方式,实现的实例化对象时被static修饰的,属于类,不属于对象,在对象创建前就已经创建好。
- 另外:不能再单例类中添加类初始化标记为或计数值(如:Boolean flag等)来防止此类攻击,因为通过反射仍然可以随意修改他们的值。
- 上述不能用反射创建枚举类原因:
【newInstance源码】:
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
上述源码可以看出无法通过反射创建枚举实例,也即创建枚举实例只有编译器能够做到。
**【防御反射攻击】**使用反射强行调用私有构造器时:,解决方式可以修改构造器,让它在创建第二个实例的时候抛异常
private Singleton(){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists !");
}
}
2.2 序列化攻击
每次反序列化一个序列化的对象实例时都会创建一个新的实例
package attack_singal;
import create_singal.DoubleCheckLockSingleton;
import java.io.*;
/*
* 破坏单例模式 - 序列化攻击
* */
public class SingletonAttack2 {
public static void main(String[] args) throws Exception {
serializationAttack();
}
private static void serializationAttack() throws Exception {
//创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serFile"));
//获取双重检查锁实现类的单例对象
DoubleCheckLockSingleton instance = DoubleCheckLockSingleton.getInstance();
//序列化单例对象
oos.writeObject(instance);
//创建反序列化流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("serFile")));
//读取反序列化的对象
DoubleCheckLockSingleton instance2 = (DoubleCheckLockSingleton)ois.readObject();
//调用对象方法
instance.tell();
instance2.tell();
System.out.println(instance == instance2);
}
}
执行结果:
This is a DoubleCheckLockSingleton 1173230247
This is a DoubleCheckLockSingleton 81628611
false
为什么会发生这种事?在ObjectInputStream.readObject()方法执行时,其底层源码调用了内部方法readOrdinaryObject()中有这样一句话:
obj = desc.isInstantiable() ? desc.newInstance() : null;
其中:desc:是类描述符。上述三元表达式的作用是:一个实现了Serializable/Externalizable接口的类可以在运行时实例化,就调用newInstance()方法,通过反射技术调用其默认构造方法创建新的对象实例。一旦有新实例被创建就会破坏单例性(因为序列化时已经创建过一个实例了),通过反序列,一个新的对象克隆了出来。
【防御序列化攻击:】,就得将instance声明为transient,并且在单例类中加入以下语句。
private static void serializationAttack() throws Exception{
//上述其余代码不变
private Object readResolve() {
return instance;
}
}
说明:
将instance声明为transient:将对象定义为瞬态变量,防止序列化
加入上述语句:当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了,单例规则也就得到了保证。
因为:在上述的readOrdinaryObject()方法中,通过desc.hasReadResolveMethod()检查类中是否存在名为readResolve()的方法,如果有,就执行desc.invokeReadResolve(obj)调用readResolve()方法。readResolve()会用自定义的反序列化逻辑覆盖默认实现,因此强制它返回instance本身,就可以防止产生新的实例。
3. 枚举单例的防御机制
3.1 对反射的防御
- 实例:直接将上述reflectionAttack()方法中的类名改成EnumSingleton并执行,会发现报如下异常:
Exception in thread "main" java.lang.NoSuchMethodException: create_singal.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at attack_singal.SingletonAttack.reflectionAttack(SingletonAttack.java:17)
at attack_singal.SingletonAttack.main(SingletonAttack.java:10)
这是因为所有Java枚举都隐式继承自Enum抽象类,而Enum抽象类根本没有无参构造方法,只有如下一个构造方法:
protected Enum(String name, int ordinal) {//只能由编译器调用
this.name = name;
this.ordinal = ordinal;
}
加入改成获取有参构造呢?依然会报异常
Exception in thread "main" java.lang.NoSuchMethodException: create_singal.EnumSingleton.<init>(java.lang.String, java.lang.Integer)
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at attack_singal.SingletonAttack.reflectionAttack(SingletonAttack.java:17)
at attack_singal.SingletonAttack.main(SingletonAttack.java:10)
来到Constructor.newInstance()方法中,有如下语句:
//这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
可见,JDK反射机制内部完全禁止了用反射创建枚举实例的可能性。
说明:
getModifiers()方法:作用以整数形式返回该方法或该成员变量的修饰符。
其中不同的修饰符所对应的数值不同,修饰符的值与java.lang.reflect.Modifier类中的静态方法和常量的编码有关。
【补充】单例模式应用:线程池,缓存,日志对象,对话框对象,目的:为了避免不一致状态。
3.2 对序列化的防御
如果将serializationAttack()方法中的攻击目标换成EnumSingleton,那么我们就会发现s1和s2实际上是同一个实例,最终会打印出true。这是因为ObjectInputStream类中,对枚举类型有一个专门的readEnum()方法来处理,其简要流程如下:
- 通过类描述符取得枚举单例的类型EnumSingleton;
- 取得枚举单例中的枚举值的名字(这里是INSTANCE);
- 调用Enum.valueOf()方法,根据枚举类型和枚举值的名字,获得最终的单例。
这种处理方法与readResolve()方法大同小异,都是以绕过反射直接获取单例为目标。不同的是,枚举对序列化的防御仍然是JDK内部实现的。
【小结】:
综上我们推荐使用枚举单例模式。但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。