前言:
单例模式并不是完全安全的(枚举式除外),因为在Java中可以通过反射和反序列化来破解单例模式的实现方式,具体原理如下所示(单例模式以懒汉式为例):
1、利用反射破解单例:
代码演示:
public class ClientTest {
public static void main(String[] args) throws Exception{
SingletonDemo s1 = SingletonDemo.getInstance();
SingletonDemo s2 = SingletonDemo.getInstance();
System.out.println(s1);
System.out.println(s2);
//通过反射的方式直接调用私有构造器
Class<SingletonDemo> clazz = (Class<SingletonDemo>)Class.forName("singleton.SingletonDemo");
Constructor<SingletonDemo> constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
SingletonDemo s3 = constructor.newInstance();
SingletonDemo s4 = constructor.newInstance();
System.out.println(s3);
System.out.println(s4);
}
}
s1和s2是通过单例对象调用方法获得单例实例,输出结果一致。而s3和s4是通过反射获得的对象,它们的输出结果与s1、s2不同。
输出结果如下:
singleton.SingletonDemo02R@6d06d69c
singleton.SingletonDemo02R@6d06d69c
singleton.SingletonDemo02R@7852e922
singleton.SingletonDemo02R@4e25154f
如何防止他人利用反射来获取单例对象呢?
可以在构造方法中手动抛出异常控制,原理就是在第二次创建单例的时候判断单例是否为null,如果不为null的情况,手动抛出一个RuntimeException()。
代码演示:
public class SingletonDemo{
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo instance;
//初始化构造器
private SingletonDemo(){
if(instance!=null){
throw new RuntimeException("实例已存在,不能再建!");
}
}
//方法同步,调用效率低
public static synchronized SingletonDemo getInstance(){
if(instance == null){
instance = new SingletonDemo();
}
return instance;
}
}
当在单例模式中手动跑出异常控制后,输出结果会出现异常,具体如下所示:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at singleton.ClientTest.main(ClientTest.java:22)
Caused by: java.lang.RuntimeException: 实例已存在,不能再建!
at singleton.SingletonDemo02R.<init>(SingletonDemo02R.java:12)
... 5 more
singleton.SingletonDemo02R@6d06d69c
singleton.SingletonDemo02R@6d06d69c
2、利用反序列化破解单例:
在利用反序列化进行破解单例的时候,首先需要在原单例模式上实现接口Serializable,因为如果不实现接口的话,会出现 java.io.NotSerializableException的异常。具体代码如下:
public class SingletonDemo implements Serializable{//实现Serializable接口
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo instance;
//初始化构造器
private SingletonDemo(){
if(instance!=null){
throw new RuntimeException("实例已存在,不能再建!");
}
}
//方法同步,调用效率低
public static synchronized SingletonDemo getInstance(){
if(instance == null){
instance = new SingletonDemo();
}
return instance;
}
}
public class ClientTest {
public static void main(String[] args) throws Exception{
SingletonDemo s1 = SingletonDemo.getInstance();
SingletonDemo s2 = SingletonDemo.getInstance();
System.out.println(s1);
System.out.println(s2);
//通过反序列化的方式构造多个对象
FileOutputStream fos = new FileOutputStream("d:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
FileInputStream fis = new FileInputStream("d:/a.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
SingletonDemo s3 = (SingletonDemo)ois.readObject();
ois.close();
fis.close();
System.out.println(s3);
}
}
输出结果如下:
singleton.SingletonDemo02R@6d06d69c
singleton.SingletonDemo02R@6d06d69c
singleton.SingletonDemo02R@119d7047
如何防止他人利用反射来获取单例对象呢?
反序列化时,会在原单例实现模式中调用readResolve()这个方法,这时,直接返回此方法指定的对象,而不需要单独再创建新对象。
代码演示:
public class SingletonDemo02R implements Serializable{
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo02R instance;
//初始化构造器
private SingletonDemo02R(){
if(instance!=null){
throw new RuntimeException("实例已存在,不能再建!");
}
}
//方法同步,调用效率低
public static synchronized SingletonDemo02R getInstance(){
if(instance == null){
instance = new SingletonDemo02R();
}
return instance;
}
//反序列化时,会直接调用readResolve()这个方法
private Object readResolve() throws Exception{
return instance;
}
}
这时输出结果如下:
singleton.SingletonDemo02R@6d06d69c
singleton.SingletonDemo02R@6d06d69c
singleton.SingletonDemo02R@6d06d69c
3、总结
反射可以破解上一条博客提到的几种(不包含枚举式)实现方式!
可以在构造方法中手动抛出异常控制
反序列化也可以破解上一条博客提到的几种(不包含枚举式)实现方式!
可以通过定义readResolve()防止获得不同对象。反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象。
4、针对上一条博客测试效率代码CountDownLatch类(闭锁)的补充说明
CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。它有两个常用的方法:
countDown():当前线程调用此方法,则计数减一(减一要放在finally里面执行)。
await():调用此方法会一直阻塞当前线程,直到计时器的值为0。