前言
大家都知道单例模式,单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。具体做法是将构造器私有化,不让其他类调用构造器,即其他类不能通过new的方式创建对象。但是反射 ,序列化,克隆等操作都有可能会破坏单利模式。
现在看一个问题,创建一个类的对象有几种方式呢?
- new
- 反射
- 序列化
- 克隆(或称为复制,拷贝):又分为深克隆和浅克隆。
克隆对单例的破坏
由克隆我们可以想到原型模式,原型模式就是**根据给定某个类的对象(称为原型),复制出该类的另一个对象,复制的对象与原型对象有着相同的变量值。**我们可以直接手动去new一个新的对象,然后set原对象的属性值到新对象的属性中,也可以直接通过重写Object类中的clone方法。
就是通过clone方法实现对象的创建的,clone方法是Object类中protected方法。我们知道,protected方法子类直接调用,哪怕不是同一包下也能调用。
但是Object类中的clone方法特殊,不能够让子类直接调用。一般都都是子类重写该方法,并且子类需要实现Cloneable接口,这样才可以对该类进行克隆,如果子类不实现Cloneable接口,那么在调用clone方法会抛出CloneNotSupportedException异常
public class Person implements Cloneable{
private Integer id;
private String name;
private BirDate birDete;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
//省略get,set方法
}
/**
* 浅克隆。
* @throws Exception
*/
@Test
public void test01() throws Exception {
Person p=new Person(1,"青冰白夜",new BirDate("1","2"));
/**
* 虽然说子类,可以调用父类的protected方法,但是Object类中的clone方法特殊,需要子类重写该方法,然后
* super.clone()
*/
Person p2= (Person) p.clone();
System.out.println(p);
System.out.println(p2);
}
//执行结果如下:
Person{id=1, name='青冰白夜', birDete=com.zxc.pattern.原型模式.BirDate@4ee285c6}
Person{id=1, name='青冰白夜', birDete=com.zxc.pattern.原型模式.BirDate@4ee285c6}
如果是这样的话,那么上面说的单例模式失效了?当然答案是否定,某一个对象直接调用clone方法,会抛出异常,即并不能成功克隆一个对象。调用该方法时,必须实现一个Cloneable 接口。这也就是原型模式的实现方式。还有即如果该类实现了Cloneable接口,尽管构造函数是私有的,他也可以创建一个对象。即clone方法是不会调用构造函数的,他是直接从内存中copy内存区域的。所以单例模式的类是不可以实现Cloneable接口的。
反射对单例的破坏
- 破坏单例实例代码
/**
@author 青冰白夜
最简单的单例模式,这里就先不考虑线程安全问题,主要是描述下单例破坏的情况
*/
public class Singleton {
private Integer id;
private Singleton(){
System.out.println("private Singleton()");
}
private static Singleton instance=null;
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
/**
测试类
*/
@Test
public void test02() throws Exception {
Singleton s=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
System.out.println(s==s2);
Class c=s.getClass();
//获取私有构造器
Constructor<Singleton> cons=c.getDeclaredConstructor();
//设置暴力访问
cons.setAccessible(true);
Singleton s3=cons.newInstance();
System.out.println(s==s3);
}
//执行结果如下:
private Singleton()
true
private Singleton()
false
单例模式的构造方法除了加上 private 以外,没有做任何处理。如果我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会两个不同的实例。由上面的测试可知,即便将构造器私有化了,还是会被反射调用,反射就这样破坏了单例。下面介绍解决方法。
- 第一种解决方法:基本思路是在构造器里判断实例是否为null,如果不是直接抛出异常
/**
* @author zhouxingcan
*/
public class Singleton {
private Integer id;
private static Singleton instance=null;
private Singleton(){
if(instance!=null){
throw new RuntimeException("老子是单例,你别想破坏!");
}
System.out.println("private Singleton()");
}
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
上述代码考虑的是,在构造器中,判断实例是否为null,若不是就直接抛出异常。在这里很多人可能会说这样看似可以防止反射调用私有构造,其实还有一点需要注意,反射照样可以修改私有变量的值,即可以修改实例变量instance为null,这样照样可以调用私有构造器了。
这么说是没有错的,但是呢,正常情况下,外界是不知道该单例的实例变量的名字。我也查阅了很多资料,也都没有提到这个问题,所有在此可以先忽略。如果后续发现更好的方式,会持续更新博客。
@Test
public void test02() throws Exception {
Singleton s=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
Class c=s.getClass();
Field field=c.getDeclaredField("instance");
field.setAccessible(true);
//这里直接将单Singleton类中的instance变量设置为null,即可“骗过”构造器里的判断。
//其实正常情况下,外界是不知道这个实例变量的名字,所以这种方式基本可以忽略
field.set(s,null);
System.out.println(s+"--"+s2);
//获取私有构造器
Constructor<Singleton> cons=c.getDeclaredConstructor();
//设置暴力访问
cons.setAccessible(true);
Singleton s3=cons.newInstance();
System.out.println(s==s3);
}
//执行结果如下:
private Singleton()
true
com.zxc.pattern.原型模式.Singleton@621be5d1--com.zxc.pattern.原型模式.Singleton@621be5d1
private Singleton()
false
- 第二种解决方式
使用内部类的方式:当外部类加载时,会优先加载内部类。具体看代码
/**
使用内部类的方式,
*/
public class LazyInnerClassSingleton {
//默认使用 LazyInnerClassGeneral 的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private LazyInnerClassSingleton(){
if(LazyHolder.LAZY != null){
throw new RuntimeException("不允许创建多个实例");
}
}
//每一个关键字都不是多余的
//static 是为了使单例的空间共享
//保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
这种内部类实现单例的方式,是绝对线程安全的,既可以防止高并发情况下线程安全问题,又可以实现保护单例。
序列化破坏单例
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到物理内存,下次使用时再从物理内存中读取到对象,反序列化转化为JVM内存对象。反序列化后的对象会重新分配JVM内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。
来看一段代码:
/**
需要序列化的类
*/
public class SingletonSerializable implements Serializable{
private static SingletonSerializable instance=null;
private SingletonSerializable(){}
public static SingletonSerializable getInstance(){
if(instance==null){
instance=new SingletonSerializable();
}
return instance;
}
}
/**
测试类
*/
public class SingletonDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException, CloneNotSupportedException {
SingletonSerializable s1=SingletonSerializable.getInstance();
//序列化
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(s1);
//反序列化
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
SingletonSerializable s2= (SingletonSerializable) ois.readObject();
System.out.println(s1);
System.out.println(s2);
}
}
//输出结果如下:
//com.zxc.pattern.单例模式.SingletonSerializable@330bedb4
//com.zxc.pattern.单例模式.SingletonSerializable@16b98e56
}
运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加 readResolve()方法即可。来看优化代码:
public class SingletonSerializable implements Serializable{
private static SingletonSerializable instance=null;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
private SingletonSerializable(){
}
public static SingletonSerializable getInstance(){
if(instance==null){
instance=new SingletonSerializable();
}
return instance;
}
private Object readResolve(){
return instance;
}
}
再看运行结果
com.zxc.pattern.单例模式.SingletonSerializable@45ee12a7
com.zxc.pattern.单例模式.SingletonSerializable@45ee12a7
两个是同一个对象,说明可行。看起来简单,但是为什么呢?这个等后续更新注册式单例,以及jdk中ObjectOutputStream源码分析才能深入理解原因,具体后续分享。