vlambda博客
学习文章列表

有意思的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");

这就说明了通过反射是不能创建枚举实例的,所以这种方式可以很好的避免类似前面的通过反射的破坏,另外由于枚举的特性,也能很好的保证单例的线程安全性和避免被反序列破坏。

因此,单元素的枚举类型是我们实现单例模式的最佳方法!