说到单例设计模式,大家应该都比较熟悉,也能说个一二三,单例单例,无非就是 保证一个类只有一个对象实例嘛,一般就是私有化构造函数,然后再暴露一个方法提供一个实例,确实没错,但是怎么样保证一个单例的安全性呢,私有构造函数,那如果反射强势调用呢?再比如序列化一个对象后,反序列化呢?生成的对象是否还是一样的?
单例模式现在的写法确实也是有蛮多种,总结一下,大概有如下几种:
那么每种写法到底有什么区别呢?哪种才是最适合的,话不多说,直接撸代码~
/** * @FileName: com.example.hik.lib * @Desription: 描述功能 * @Anthor: taolin * @Version V2.0 <描述当前版本功能> */ public class SingleInstanceDemo { private static SingleInstanceDemo sSingleInstanceDemo; private SingleInstanceDemo(){ } public synchronized static SingleInstanceDemo getInstance(){ if (sSingleInstanceDemo==null) sSingleInstanceDemo = new SingleInstanceDemo(); return sSingleInstanceDemo; } } 复制代码
代码很简单,这种方式是线程安全的,但是很明显,每次调用方法,都需要先获得同步锁,性能比较低,不建议这么写
/** * @FileName: com.example.hik.lib * @Desription: 描述功能 * @Anthor: taolin * @Date: 2019/2/21 * @Version V2.0 <描述当前版本功能> */ public class SingleInstanceDemo { private static SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo(); private SingleInstanceDemo(){ } public synchronized static SingleInstanceDemo getInstance(){ return sSingleInstanceDemo; } } 复制代码
这种写法,不能确保你的实例是在调用getInstance方法时生成的,因为类的加载机制是在可能需要使用到这个类的时候就加载(比如其他地方引用到了这个类名等等),不清楚的可以看下上篇文章静态变量的生命周期,所以这种也不能达到懒加载的效果。
/** * @FileName: com.example.hik.lib * @Desription: 描述功能 * @Anthor: taolin * @Date: 2019/2/21 * @Version V2.0 <描述当前版本功能> */ public class SingleInstanceDemo { private static SingleInstanceDemo sSingleInstanceDemo; private SingleInstanceDemo(){ } public static SingleInstanceDemo getInstance(){ if (sSingleInstanceDemo==null){ synchronized (SingleInstanceDemo.class){ if (sSingleInstanceDemo==null){ sSingleInstanceDemo = new SingleInstanceDemo(); } } } return sSingleInstanceDemo; } } 复制代码
可以看到,把synchronized关键字是移到了内部,保证不用每次调用方法都得获取同步锁,性能有一定的提升,但是有一个问题,在Java指令中,对象的创建和赋值不是一步操作的,JVM会对代码进行一定的指令重排序(具体规则就不多介绍了,自行google),也就是说可能JVM会先直接赋值给instance成员,然后再去初始化这个sSingleInstanceDemo实例,这样就会出现问题
当然也就解决办法,加上volatile关键字就好了,可以禁止指令重排序
/** * @FileName: com.example.hik.lib * @Desription: 描述功能 * @Anthor: taolin * @Date: 2019/2/21 * @Version V2.0 <描述当前版本功能> */ public class SingleInstanceDemo { public static class InnerClass{ private static final SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo(); } private SingleInstanceDemo(){ } public static SingleInstanceDemo getInstance(){ return InnerClass.sSingleInstanceDemo; } } 复制代码
乍一看!咦,好像和饿汉式有点像,只不过这里声明了一个私有的静态内部类,这样的区别就在于:
静态sSingleInstanceDemo对象的生成一定是在调用getInstance()方法的时候生成的,因为它是跟随着InnerClass这个类的加载而产生的,它本身是一个私有类,也保证了不会有其他的地方来调用InnerClass,这种写法比较推荐
/** * @FileName: com.example.hik.lib * @Desription: 描述功能 * @Anthor: taolin * @Date: 2019/2/21 * @Version V2.0 <描述当前版本功能> */ public enum SingleInstanceDemo { INSTANCE; private SingleInstanceDemo() { } } 复制代码
单例的枚举实现在《Effective Java》中有提到,因为其 功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化 等优点,单元素的枚举类型被认为是实现Singleton的最佳方法。
但是枚举类就内存消耗是比正常类要大的,所以,看情况选择适合自己的最好
我们先来写个demo来看看,是不是反射和反序列化真的会导致单例模式的问题
package com.example.hik.lib; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; public class MyClass { public static void main(String[] args) throws IOException, ClassNotFoundException { //我们通过静态内部类方式,获取单例对象 SingleInstanceDemo instance = SingleInstanceDemo.getInstance(); //通过反射来获取一个对象 SingleInstanceDemo instance2 = null; Class<SingleInstanceDemo> singleInstanceDemoClass = SingleInstanceDemo.class; try { Constructor<SingleInstanceDemo> constructor = singleInstanceDemoClass.getDeclaredConstructor(null); constructor.setAccessible(true); instance2 = constructor.newInstance(); } catch (Exception mE) { mE.printStackTrace(); } System.out.println("reflect obj :"+(instance==instance2)); // 1. 把对象instance写入硬盘文件 FileOutputStream fos = new FileOutputStream("object.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance); oos.close(); fos.close(); // 2. 把硬盘文件上的对象读出来 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt")); SingleInstanceDemo instance3 = (SingleInstanceDemo) ois.readObject(); System.out.println("Deserialize obj :"+(instance==instance3)); } } 复制代码
run一下上面的代码可以看到
reflect obj :false Deserialize obj :false Process finished with exit code 0 复制代码
居然都是false,也就是我们通过反射和反序列生成的对象和单例对象是不一样的,那么岂不是单例就不是单例的意义了,我们来改进一下代码
/** * @FileName: com.example.hik.lib * @Desription: 描述功能 * @Anthor: taolin * @Date: 2019/2/21 * @Version V2.0 <描述当前版本功能> */ public class SingleInstanceDemo implements Serializable { public static class InnerClass{ public static final SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo(); } private SingleInstanceDemo(){ if (null!=InnerClass.sSingleInstanceDemo){ throw new RuntimeException("不要用反射哦"); } } public static SingleInstanceDemo getInstance(){ return InnerClass.sSingleInstanceDemo; } private Object readResolve() throws ObjectStreamException { return InnerClass.sSingleInstanceDemo; } } 复制代码
解决办法:
再Run一下主代码,可以看到
java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.example.hik.lib.MyClass.main(MyClass.java:20) Caused by: java.lang.RuntimeException: 不要用反射哦 at com.example.hik.lib.SingleInstanceDemo.<init>(SingleInstanceDemo.java:19) ... 5 more Deserialize obj :true Process finished with exit code 0 复制代码
反射会抛出异常,而反序列化后对象也是和之前的单例是一样的,这样就大功告成了~
主要还是希望小伙伴能真正弄清楚每个单例模式的意义和不足之处,这样不管是在面试还是在日常开发中能够更好的掌握单例模式~比心❤