破坏单例模式的原因

前言

大家都知道单例模式,单例模式(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源码分析才能深入理解原因,具体后续分享。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值