单例模式——顾名思义即在既定的业务场景下某一实体类只需存在一个对象,就能充分的处理所有的业务需求。而且在某种现场环境下,创建这样的对象对系统性能的开销非常大。正因为这种特性,单利模式通常具有节省系统开销的效果。我将从以下几个方面对一些常见的单利模式进行总结归纳,在下才疏学浅,不曾卖弄,旨在知识重温与记录。有所疏忽,请各位不吝指正,自当感激不尽。
归纳层面:
常见的单利模式以及实现方式。
产品级单例模式的穿透方式以及防范方法。
常见的单利模式的并发性能测试。
一,常见的单利模式以及实现方式
在实现层面上,目前的几种主要的单例模式往往有以下几项性能指标作为选型参考:
-- 是否实现延迟加载
-- 是否线程安全
-- 并发访问性能
-- 是否可以防止反射与反序列化穿透
经过一段时间的工作和学习,将自己所遇到的几种单例模式作如下比较总结,当然,也作为自己学习复习的一种方式。
<1>,饿汉式单例模式。
/** * 未实现延迟加载 * 线程安全 * @author xinz * */ public class Singleton1 { private Singleton1(){} private static Singleton1 instance = new Singleton1(); public static Singleton1 getInstance(){ return instance; } }
<2>,懒汉式单例模式
/** * 实现延迟加载 * 线程安全但牺牲高并发性能 * @author xinz */ public class Singleton2 { private Singleton2(){ } private static Singleton2 instance; public static synchronized Singleton2 getInstance(){ if(instance == null){ instance = new Singleton2(); } return instance; } }
<3>,双重检测锁式单例模式
/** * 双重检测锁式单例模式 * 实现了延迟加载 * 线程安全 * @author xinz * */ public class Singleton3 { private static Singleton3 instance = null; private Singleton3() {} public static Singleton3 getInstance() { if (instance == null) { Singleton3 sc; synchronized (Singleton3.class) { sc = instance; if (sc == null) { synchronized (Singleton3.class) { if (sc == null) { sc = new Singleton3(); } } instance = sc; } } } return instance; } }
<4>,静态内部类式单例模式
/** * 静态内部类单利模式 * 线程安全 * 实现延迟加载 * @author xinz * */ public class Singleton4 { private Singleton4 (){} /** * 外部类初始化的时候不会初始化该内部类 * 只有当调用getInstance方法时候才会初始化 */ public static class inner{ public static final Singleton4 instance = new Singleton4(); } public static Singleton4 getInstance(){ return inner.instance; } }
<5>,枚举式单例模式
/** * 未延迟加载 * 线程安全 * 原生防止反射与反序列话击穿 * @author xinz */ public enum Singleton5 { INSTANCE; public static Object doSomething(){ //添加其他功能逻辑。。。。。。 return null; } }
对于以上5种单例模式作如下简单测试:
/** * 测试单利是否返回相同对象 * @author xinz * */ public class TestSingleton { public static void main(String[] args) { /** * 饿汉式 */ Singleton1 singleton1_1 = Singleton1.getInstance(); Singleton1 singleton1_2 = Singleton1.getInstance(); System.out.println(singleton1_1 == singleton1_1);//true /** * 懒汉式 */ Singleton2 singleton2_1 = Singleton2.getInstance(); Singleton2 singleton2_2 = Singleton2.getInstance(); System.out.println(singleton2_1 == singleton2_1);//true /** * 双重检测锁式 */ Singleton3 singleton3_1 = Singleton3.getInstance(); Singleton3 singleton3_2 = Singleton3.getInstance(); System.out.println(singleton3_1 == singleton3_1);//true /** * 静态内部类式 */ Singleton4 singleton4_1 = Singleton4.getInstance(); Singleton4 singleton4_2 = Singleton4.getInstance(); System.out.println(singleton4_1 == singleton4_1);//true /** * 枚举式 */ Singleton5 singleton5_1 = Singleton5.INSTANCE; Singleton5 singleton5_2 = Singleton5.INSTANCE; /* * 枚举型的任何成员类型都是类实例的类型 */ System.out.println(singleton5_1.getClass());//class com.xinz.source.Singleton5 System.out.println(singleton5_1 == singleton5_1);//true } }
综上,5种实现单例模式的方法,都能基本实现现有系统目标对象的唯一性。区别在于是否能够延迟加载进一步节约系统性能。其中“双重检测锁式”由于JVM底层在执行同步块的嵌套时有时会发生漏洞,所以在JDK修复该漏洞之前,该方式不建议使用。
二,产品级单例模式的穿透方式以及防范方法
关于以上的五种单利模式的实现方式,对一般的Web应用开发,我们无需考虑谁会来试图破解我们的单利限制。但如果开发是面向产品级,那么我们将不得不考虑单例破解问题,常见的单例模式多见于反射穿透与序列化破解。
<1>,防止反射穿透。
对于反射,我们知道只要有构造方法,不做处理的情况下,即使私有化构造器,也没办阻止反射调用得到对象。从而使既有系统存在多个对象。如下,我们使用饿汉式单例模式为例,进行反射穿透。代码如下:
/* * 反射破解饿汉式单例模式 */ public class TestReflectSingleton { public static void main(String[] args) throws Exception { Class<Singleton1> clazz = (Class<Singleton1>) Class.forName("com.xinz.source.Singleton1"); Constructor<Singleton1> constructor = clazz.getDeclaredConstructor(null); //强制设置构造器可访问 constructor.setAccessible(true); Singleton1 s1 = constructor.newInstance(); Singleton1 s2 = constructor.newInstance(); System.out.println(s1==s2);//false } }
那么很显然,像饿汉式,懒汉式,双重检测锁式,静态内部类事,这几种只要有构造器的单例模式就会存在被反射穿透的风险。而第五种枚举式单例模式,原生不存在构造器,所以避免了反射穿透的风险。
对于前边四种存在反射穿透的单例模式,我们的解决思路就是,万一有人通过反射进入到构造方法,那么我们可以考虑抛异常,代码如下:
/** * 实现延迟加载 * 线程安全但牺牲高并发性能 * @author xinz */ public class Singleton2 { /* * 如果有反射进入构造器,判断后抛异常,这样的话一旦初始化 instance 对象 * 反射调用便会被阻止,初始化之前还是可以被反射的 */ private Singleton2(){ if(instance != null){ throw new RuntimeException(); } } private static Singleton2 instance; public static synchronized Singleton2 getInstance(){ if(instance == null){ instance = new Singleton2(); } return instance; } }
测试代码:
import java.lang.reflect.Constructor; public class TestSingleton { public static void main(String[] args) throws Throwable { Singleton2 s1 = Singleton2.getInstance(); Singleton2 s2 = Singleton2.getInstance(); System.out.println(s1); System.out.println(s1); Class<Singleton2> clazz = (Class<Singleton2>) Class.forName("com.xinz.source.Singleton2"); Constructor<Singleton2> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Singleton2 s3 = c.newInstance(); Singleton2 s4 = c.newInstance(); System.out.println(s3); System.out.println(s4); } }
执行结果:
com.xinz.source.Singleton2@2542880d com.xinz.source.Singleton2@2542880d Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:526) at com.xinz.source.TestSingleton.main(TestSingleton.java:17) Caused by: java.lang.RuntimeException at com.xinz.source.Singleton2.<init>(Singleton2.java:15) ... 5 more
即一旦初始化完成后,反射就会报错。但无法阻止反射发生在初始化之前,代码如下:
import java.lang.reflect.Constructor; public class TestSingleton { public static void main(String[] args) throws Throwable { Class<Singleton2> clazz = (Class<Singleton2>) Class.forName("com.xinz.source.Singleton2"); Constructor<Singleton2> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Singleton2 s3 = c.newInstance(); Singleton2 s4 = c.newInstance(); System.out.println(s3); System.out.println(s4); Singleton2 s1 = Singleton2.getInstance(); Singleton2 s2 = Singleton2.getInstance(); System.out.println(s1); System.out.println(s1); } }
测试结果如下:
com.xinz.source.Singleton2@32f22097
com.xinz.source.Singleton2@3639b3a2
com.xinz.source.Singleton2@6406c7e
com.xinz.source.Singleton2@6406c7e
很显然反射得到的两个对象不是同一对象。目前尚未找到解决策略,还望高手指点。
<2>,反序列化破解
反序列化即先将系统里边唯一的单实例对象序列化到硬盘,然后在反序列化,得到的对象默认和原始对象属性一致,但已经不是同一对象了。如下:
/** * 反序列化创建新对象 * @author xizn */ public class TestSingleton { public static void main(String[] args) throws Throwable { Singleton2 s1 = Singleton2.getInstance(); System.out.println(s1); //通过反序列化的方式构造多个对象 FileOutputStream fos = new FileOutputStream("d:/a.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s1); oos.close(); fos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt")); Singleton2 s3 = (Singleton2) ois.readObject(); System.out.println(s3); } }
测试结果如下:(当然,目标类要实现序列化接口)
com.xinz.source.Singleton2@3639b3a2
com.xinz.source.Singleton2@46e5590e
如何防止这种破解单利模式,我们采取重写反序列化方法 -- readResolve() 最终防止单利被破解的代码如下(这里仅以懒汉式为例,其它类似):
import java.io.ObjectStreamException; import java.io.Serializable; /** * 实现延迟加载 * 线程安全但牺牲高并发性能 * @author xinz */ public class Singleton2 implements Serializable { /* * 如果有反射进入构造器,判断后抛异常,这样的话一旦初始化 instance 对象 * 反射调用便会被阻止,初始化之前还是可以被反射的 */ private Singleton2(){ if(instance != null){ throw new RuntimeException(); } } private static Singleton2 instance; public static synchronized Singleton2 getInstance(){ if(instance == null){ instance = new Singleton2(); } return instance; } //反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象! private Object readResolve() throws ObjectStreamException { return instance; } }
还是上边的测试代码,测试结果:
com.xinz.source.Singleton2@6f92c766
com.xinz.source.Singleton2@6f92c766
三,常见的单利模式的并发性能测试
测试我们启用20个线程,每个线程循环获取单例对象100万次,测试代码:
/** * 并发性能测试 * @author xizn */ public class TestSingleton { public static void main(String[] args) throws Throwable { long start = System.currentTimeMillis(); int threadNum = 20; final CountDownLatch countDownLatch = new CountDownLatch(threadNum); for(int i=0;i<threadNum;i++){ new Thread(new Runnable() { @Override public void run() { for(int i=0;i<1000000;i++){ // Object o1 = Singleton1.getInstance(); // Object o2 = Singleton2.getInstance(); // Object o3 = Singleton3.getInstance(); // Object o4 = Singleton4.getInstance(); Object o5 = Singleton5.INSTANCE; } countDownLatch.countDown(); } }).start(); } countDownLatch.await(); //main线程阻塞,直到计数器变为0,才会继续往下执行! long end = System.currentTimeMillis(); System.out.println("总耗时:"+(end-start)); } }
执行结果根据电脑性能每个人可能会有不同的结果,但大概还是可以反映出性能优劣:
饿汉式 | 总耗时:10毫秒 | 不支持延迟加载,一般不能防范反射与反序列化 |
懒汉式 | 总耗时:498毫秒 | 支持延迟加载,一般不能防范反射与反序列化,并发性能差 |
双重检测锁式 | 总耗时:11毫秒 | JVM底层支持不太好,其它性能同饿汉式 |
静态内部类式 | 总耗时:12毫秒 | 一般不能防范反射与反序列化,其它性能良好 |
枚举式 | 总耗时:12毫秒 | 未实现延迟加载,原生防范反射与反序列化,其它性能良好 |
综上测试结果,个人认为:
对于要求延迟加载的系统,静态内部类式优于懒汉式。
对于产品级别,要求安全级别高的系统,枚举式优于饿汉式。
双重检测锁式单例模式在JDK修复同步块嵌套漏洞之前不推荐。
写了大半天,总算对自己的学习内容总结告一段落,在此,特别感谢高淇、白鹤翔两位老师。