单例设计模式总结-5种常见写法+防止发射反序列化

单例模式是设计模式中最常见的,也是最简单的一种,所谓单例,是需要在任何时候只存在一个对象实例,故显然需要私有化构造器,构造器私有了,要想获得这个实例,故必须在类内部创建对象实例,同时必须提供静态方法来获取,静态方法只能操作静态属性,故内部对象实例需要被static修饰,由于单例,可用final修饰;

单例存在多种写法,有各自不同的特点,下面介绍常用的写法,并且这些写法有些存在漏洞,如发射、发序列化可以破坏该单例;

一、单例模式的五种写法

1、饿汉式

public class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton(){
	}
	public static Singleton getInstance(){
		return instance;
	}
}
饿汉式在类加载后,直接创建了对象,从文章开头的解释出发,可以理解为什么用private、static这些关键字

2、懒汉式

class Singleton2{
	private static Singleton2 instance;
	private Singleton2(){}
	public static Singleton2 getInstance(){
		if(instance==null){ 		//A位置
			instance = new Singleton2();
		}
		return instance;
	}
}
懒汉式具有延迟加载的特点,即在需要用该对象的时候才会创建实例,在一定程度上可以节约点资源;

懒汉式vs饿汉式:饿汉式在类加载后直接创建对象(即使不需要使用对象),所以线程安全
懒汉式在需要使用时才会创建对象,非线程安全
工程中:建议使用饿汉式

懒汉式的问题:并发访问时,T1执行到A处暂停,T2同样执行到A处,并继续往下执行,
T2实例化了instance,T2执行完,T1线程继续执行,此时T1线程会继续执行instance = new Singleton2();
无法保证单例

3、单例双检锁模式

class Singleton3{
	private static volatile Singleton3 instance; //注意此处volatile关键字
	private Singleton3(){}
	public static /*synchronized*/ Singleton3 getInstance(){
		if(instance==null){  //A位置
			synchronized (Singleton3.class) {
				if(instance==null){
					instance=new Singleton3();// B位置
				}
			}
		}
		return instance;
	}
}
不加volatile关键字的双检锁模式,解决了懒汉式的线程安全问题,但它带来了新的问题

双检锁模式---存在问题(与Java内存模型有关)
理论上时很完美的,但是实际会因Java内存模型,设计指令重排序,出现问题
/**
 * 
 * 一、好处:避免在函数上使用synchronized关键字,导致每次调getIstance()函数都要
 * 读锁的开销,提高效率
 * 二、潜在问题:
 * instance=new Singleton3();分为3步
 * 1)申请空间
 * 2)初始化空间的值
 * 3)将引用instance指向该空间
 * 分析:实际应该让3步按照顺序来,但由于Java内存模型,允许他们不按顺序执行,试想:
 * T1执行B处时,初始化时按照1)->3)->2)的顺序,刚好执行完3)就被中断了,
 * 此时,T2执行到A处,判断instance==null发现instance不为null,于是
 * 将该对象返回,而该对象并未被初始化,这就导致了问题
 * 三、解决之道:单例类的成员用volatile关键字修饰,内部原理参考另一篇博客

4、静态内部类方式

class Singleton4{
	private Singleton4(){}
	private static class Singleton4Holder{
		private static Singleton4 instance = new Singleton4();
	}
	public static Singleton4 getInstance(){
		return Singleton4Holder.instance;
	}	
}

具有延迟加载特性,同时也是线程安全的,是比较推荐的写法
 1.Singleton4类被加载的时候,并不会实例化instance对象
 2.只有在调用getInstance()函数的时候,才开始加载Singleton4Holder类,并创建instance实例

5、枚举

enum Singleton5{
	INSTANCE;
	public void dosomething(){}
}
使用枚举方法的好处在于:
1.枚举天生就是线程安全的,其在任意情况下都是单例
2.枚举具有防止反射和发序列化的特点


二、单例模式防止反射和反序列化

1、防止发射,我们知道,可以通过发射方式来获取类的构造方法,并用纸创建对象,即便构造方法为private修饰的,为了防止发射的漏洞,只需在构造函数内部做个判断,如下:

	private Singleton(){
		if(null!=instance){
			throw new RuntimeException("单例已经存在");
		}
	}
2、防止反序列化

反序列化:即强对象写入磁盘再读入内存,得到一个新的实例,破坏了了单例的唯一性
  Java提供了readResolve()方法,可以让开发者控制对象的反序列化
  解决反序列化方法:在单例类中加入方法


  本质:无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。  
  实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。

private Object readResolve() throws ObjectStreamException {
		return instance;
	}
3、以饿汉式为例,设计防止发射和反序列化漏洞的单例

class Singleton6 implements Serializable{
	private static Singleton6 instance = new Singleton6();
	//防止反射破坏单例
	private Singleton6(){
		if(null!=instance){
			throw new RuntimeException("单例已经存在");
		}
	}
	//防止反序列化破坏单例
	private Object readResolve() throws ObjectStreamException {
		return instance;
	}
	public static Singleton6 getInstance(){
		return instance;
	}
}

4、以静态内部类方式为例,设计防止反射和反序列化的单例

class Singleton7 implements Serializable{
	//防止反射破坏单例模式
	private Singleton7(){
		if(null!=SingletonHolder.instance){
			throw new RuntimeException("单例已存在");
		}
	}
	//防止反序列化破坏单例模式
    private Object readResolve() throws ObjectStreamException {    
        return SingletonHolder.instance;
    }  
	
	private static class SingletonHolder{
		private static Singleton7 instance = new Singleton7();
	}
	public static Singleton7 getInstance(){
		return SingletonHolder.instance;
	}
}

总结:实际中,需要根据需要,选择合适的单例类型,从上面可以看出,一个单例涉及的知识点还是挺多的,如volatile关键字的原理和作用、线程安全问题、synchronized关键字的锁的对象是谁、反射和反序列化的原理,如何预防、类加载机制等等。

阅读更多
个人分类: 设计模式
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭