有意思的java单例模式实现:破坏与防止被破坏
作为最常用的设计模式之一单例模式,我们一般会想到这两种实现方:饿汉式和懒汉式。
饿汉式:实现简单粗暴,但因为不需要的时候就提前创建,可能存在资源的浪费,实现如下:
public class Hungry {
private static Hungry HUNGRY = new Hungry();
private Hungry(){}
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式:在真正需要的时候再去创建实例,并且保证线程安全,实现如下:
public class Lazy {
// 使用volatile防止指令重排,因为使用关键字new创建对象过程会被编译成3条指令,
// 1、根据类型分配内存空间;2、调用对象的构造函数;3、返回执行该内存的引用;
//如果没有使用volatile有可能导致指令重排,使得3比2先执行最终获得一个不完整的实例
private volatile static Lazy lazy;
private Lazy(){}
public static Lazy getInstance(){
// 双重校验,保证只有一个线程跑到第二个判空中只创建一个实例
if(lazy == null){
synchronized (Lazy.class){
if(lazy == null){
lazy = new Lazy();
}
}
}
return lazy;
}
}
但从代码安全性角度来看,单例模式是可以被破坏的,下面我们可以比较有意思的看一下单例模式如何会被破坏,以及如何防止被破坏。针对以上懒汉式的实现,最简单的就是使用反射破坏,如下:
public static void main(String[] args) throws NoSuchMethodException {
Lazy l1 = Lazy.getInstance();
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
constructor.setAccessible(true);
Lazy l2 = null;
try {
l2 = constructor.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(l1 == l2); // 这里结果输出为false,证明单例模式已经被破坏
}
有时候技术就是这么有意思,道高一尺魔高一丈,你能破坏我就能防止你被破坏,针对以上的破坏,只要原来饿汉式中的私有构造函数如下改造一下就可以了:
private Lazy(){
synchronized (Lazy.class) {
if (lazy != null) {
throw new RuntimeException("不要试图通过反射来破坏单例模式");
}
}
}
再运行以上的main函数,会发现报错:
java.lang.RuntimeException: 不要试图通过反射来破坏单例模式
但是这种加强也是很容易被破坏的,只要破坏者在获取实例的时候一开始都是通过反射来获取,就能获取到不同的实例,如下:
public static void main(String[] args) throws NoSuchMethodException {
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
constructor.setAccessible(true);
Lazy l1 = null;
Lazy l2 = null;
try {
l1 = constructor.newInstance();
l2 = constructor.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(l1 == l2); // 这里结果输出为false,证明单例模式已经被破坏
那么为了防止这种破坏,可以在单例中增加一个私有变量来达到目的,如下:
private static boolean flag = false;
private Lazy(){
synchronized (Lazy.class) {
if (flag == false) {
flag = true;
}else{
throw new RuntimeException("不要试图通过反射来破坏单例模式");
}
}
}
这样子再运行上面的main函数,可以发现再试图破坏单例模式时会报出代码中的错。那么这样是不是就完美了呢,当然不是,得益于Java反射机制的强大,只要破坏者知道单例模式实现中的flag私有变量,也是可以破坏的,看下面代码:
public static void main(String[] args) throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();
constructor.setAccessible(true);
Lazy l2 = null;
Lazy l1 = null;
try {
l1 = constructor.newInstance();
Field flag = Lazy.class.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(l1,false);
l2 = constructor.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(l1 == l2); //输出结果为false,说明单例模式已经被破坏
}
由此可以知道,只要是普通类单例模式的实现都是可能会被破坏的,那么有没有一种安全优雅的方式呢,还真有,看下面:
public enum Singleton {
INSTANCE;
public void test(){
System.out.println("This is a method ");
}
}
这种使用枚举单一元素实现的单例模式,直接调用Singleton.INSTANCE就可以获取唯一实例了。上面也可以看出,通过反射来破坏单例模式,主要是通过newInstance()来创建新的实例,但是我们点开newInstance()的实现源码可以看到其中有一行代码是
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
这就说明了通过反射是不能创建枚举实例的,所以这种方式可以很好的避免类似前面的通过反射的破坏,另外由于枚举的特性,也能很好的保证单例的线程安全性和避免被反序列破坏。
因此,单元素的枚举类型是我们实现单例模式的最佳方法!