单例模式及其反射、反序列化下的漏洞与改进

摘要

  单例模式是软件项目中最常见的设计模式之一,其主要目的是保证某些对象在项目中的唯一性。
  本文介绍了五种单例模式:饿汉式、懒汉式、双重检测式、内部静态类式、枚举式,详细介绍了它们的使用场景,给出了详细源代码,并比较了它们的执行效率。最后本文给出了五种单例模式在特殊场景下使用的缺点及其改进方法。

一、饿汉式

  类加载时,就实例化对象,以达到单例的要求,其线程安全,但不能延迟加载。

/**
 * 饿汉式
 * @author 编符侠
 * 2019-07-14
 */
public class SinglePatterns01{
	//类加载时,实例化对象
	private static SinglePatterns01 instance = new SinglePatterns01();
	
	private SinglePatterns01() {
		
	}
	
	public static SinglePatterns01 getInstance() {
		return instance;
	}
}

二、懒汉式

   具有延迟加载的特性,但是由于getInstance()方法为synchronized方法,所以当多个对象调用该方法时,存在排队等待,所以效率特别低。

/**
 * 懒汉式
 * 同步方法式
 * @author 编符侠
 * 2019-07-14
 */
public class SinglePatterns02{
	private static SinglePatterns02 instance;
	private SinglePatterns02() {
		
	}
	
	public static synchronized SinglePatterns02 getInstance() {
		if(instance == null) {
			instance = new SinglePatterns02();
		}
		return instance;
	}
}

三、双重检测式

  在懒汉式基础上,将同步方法变为同步块,可有效提高效率,同时采用双重检测,可过滤掉不需要排队的对象。

/**
 * 双重检测式
 * @author 编符侠
 * 2019-07-14
 */
public class SinglePatterns03{
	private static volatile SinglePatterns03 instance;
	private SinglePatterns03() {
		
	}
	
	public static SinglePatterns03 getInstance() {
		if(instance != null) {
			return instance;
		}
		synchronized(SinglePatterns03.class) {
			if(instance != null) {      // 会有多个线程运行到上一行,再一次判空就会防止多实例化。
				return instance;
			}
			instance = new SinglePatterns03();
		}
		return instance;
	}
}

四、内部静态类式

  类的创建具有天生的线程安全性,所以将单例封装在一个内部类中,并在内部类中采用加载时实例化对象的方式,同时采用static final保证其实例唯一性。

/**
 * 静态内部类实现方式
 * 1、外部类不是static的,所以不会像懒汉式一样提前创建对象
 * 2、类加载时线程安全的,所以把实例封装在内部类里,保证创建时的线程安全性
 * 3、内部类的目标成员变量又用static final修饰,保证了唯一性
 * 
 * 占用资源多,能延迟加载, 优于懒汉式
 * @author 编符侠
 * 2019-07-14
 */
public class SinglePatterns04{
	private static class SinglePatternsInstance{
		private static final SinglePatterns04 instance = new SinglePatterns04();
	}
	
	private SinglePatterns04() {
		
	}
	
	public static SinglePatterns04 getInstance() {
		return SinglePatternsInstance.instance;
	}
}

五、枚举式

  枚举式的单例模式代码量最少,借鉴其系统机制完成了线程安全的单例模式。其主要缺点是不能延迟加载。

/**
 * 枚举式的单例模式
 * 占用资源少,不能延迟加载,优于饿汉式
 * @author 编符侠
 * 2019-07-14
 */
public enum SinglePatterns05 {
	//这就是一个单例
	INSTANCE;
	
	public void handle() {
		//相关的类操作代码
	}
}

六、五种单例模式的性能对比

  本文采用模拟多线程多对象的情况下,去获取各个模式下的单例所耗时间。本文采用20个线程,每个线程获取100万次对象,对比其耗时结果,代码及其结果如下:

/**
 * 测试各单例模式的效率
 * @author 编符侠
 * 2019-07-15
 */
public class Client03 {
	
	public static void main(String[] args) throws Exception {		
		long start = System.currentTimeMillis();
		final int THREADNUM = 20;
		// 对线程计数,保证在其他线程结束后,主线程才结束,以此保证统计时间的正确性
		final CountDownLatch count = new CountDownLatch(THREADNUM);
		for(int i = 0; i<THREADNUM;i++){
			new Thread(
					()->{
						for(long j = 0; j<10000000; j++) {
						//  枚举式的对象获取
						//	Object instance = SinglePatterns05.INSTANCE;
						//  除枚举式的对象获取
							Object instance = SinglePatterns04.getInstance();
						}
						count.countDown();
					}
			).start();
			
		}
		count.await();
		long end = System.currentTimeMillis();
		System.out.println(end-start);
	}
}

  测试结果:

模式耗时(ms)
饿汉式254ms
懒汉式8096ms
双重检测式132ms
静态内部类式226ms
枚举式136ms

  从上表测试结果可知,懒汉式单例模式效率最低,其原因前面已经提及,它主要是有同步方法,导致其他对象想获取时,必须排队等待。

七、反射破解单例模式及其改进

7.1 反射测试单例模式

   采用反射的方式,可以发现上面的设计模式除了枚举式,其他都不能保证实例为单例,测试代码如下:

/**
 * 使用反射方式创建对象的测试
 * 除枚举式单例模式之外,其他方式的单例模式在使用反射的情况下。
 * 都不能保证单实例,需要改进。
 * 
 * 改进方法:
 * 在构造器中
 * if(null != instance){
 * 	throws new RuntimeException;
 * }
 * @author 编符侠
 * 2019-07-15
 */
public class Client {
	public static void main(String[] args) throws Exception {		
		//使用反射测试各种单例模式的安全性
		//饿汉式
		System.out.println("========= 饿汉式  =========");
		Class<SinglePatterns01> clz01 = (Class<SinglePatterns01>) Class.forName("ft.singlePatterns.SinglePatterns01");
		Constructor<SinglePatterns01> c01 = clz01.getDeclaredConstructor(null);
		c01.setAccessible(true);
		SinglePatterns01 instance11 = (SinglePatterns01) c01.newInstance();
		SinglePatterns01 instance12 = (SinglePatterns01) c01.newInstance();
		System.out.println(instance11);
		System.out.println(instance12);
		
		//懒汉式
		System.out.println("========= 懒汉式  =========");
		Class<SinglePatterns02> clz02 = (Class<SinglePatterns02>) Class.forName("ft.singlePatterns.SinglePatterns02");
		Constructor<SinglePatterns02> c02 = clz02.getDeclaredConstructor(null);
		c02.setAccessible(true);
		SinglePatterns02 instance21 = (SinglePatterns02) c02.newInstance();
		SinglePatterns02 instance22 = (SinglePatterns02) c02.newInstance();
		System.out.println(instance21);
		System.out.println(instance22);
		
		//双重检测
		System.out.println("========= 双重检测  =========");
		Class<SinglePatterns03> clz03 = (Class<SinglePatterns03>) Class.forName("ft.singlePatterns.SinglePatterns03");
		Constructor<SinglePatterns03> c03 = clz03.getDeclaredConstructor(null);
		c03.setAccessible(true);
		SinglePatterns03 instance31 = (SinglePatterns03) c03.newInstance();
		SinglePatterns03 instance32 = (SinglePatterns03) c03.newInstance();
		System.out.println(instance31);
		System.out.println(instance32);
		
		//静态内部类式
		System.out.println("========= 静态内部类式  =========");
		Class<SinglePatterns04> clz04 = (Class<SinglePatterns04>) Class.forName("ft.singlePatterns.SinglePatterns04");
		Constructor<SinglePatterns04> c04 = clz04.getDeclaredConstructor(null);
		c04.setAccessible(true);
		SinglePatterns04 instance41 = (SinglePatterns04) c04.newInstance();
		SinglePatterns04 instance42 = (SinglePatterns04) c04.newInstance();
		System.out.println(instance41);
		System.out.println(instance42);
	}
}

7.2 测试结果

  测试结果如下:

========= 饿汉式  =========
ft.singlePatterns.SinglePatterns01@279f2327
ft.singlePatterns.SinglePatterns01@2ff4acd0
========= 懒汉式  =========
ft.singlePatterns.SinglePatterns02@5caf905d
ft.singlePatterns.SinglePatterns02@27716f4
========= 双重检测  =========
ft.singlePatterns.SinglePatterns03@2a84aee7
ft.singlePatterns.SinglePatterns03@a09ee92
========= 静态内部类式  =========
ft.singlePatterns.SinglePatterns04@452b3a41
ft.singlePatterns.SinglePatterns04@4a574795

7.3 改进方法

  反射是因为拿到了创建类的图纸(Class),所以可以创建新的实例,那么改进方式是在类的构造函数中加入对象是否存在的判断即可。如下所示:

// 饿汉式单例模式的构造函数
private SinglePatterns01() {
	 if(null != instance){
 	      throws new RuntimeException;
     }
}

八、反序列化破解单例模式及其改进

  采用反序列化测试单例模式时,需要将每个模式类作为Serializable的实现类,即增加“implement Serializable”。

8.1 反序列化测试单例模式

/**
 * 使用反序列化方式创建对象的测试
 * 除枚举式单例模式之外,其他方式的单例模式在使用反序列化的情况下。
 * 都不能保证单实例,需要改进。
 * 
 * 改进方法:
 * 在各个单例模式类中重写readResolve()
 * private Object readResolve() throws ObjectStreamException{
 * 	return instance;
 * }
 * @author 编符侠
 * 2019-07-15
 */
public class Client02 {
	public static void main(String[] args) throws Exception {		
		FileOutputStream fos;
		ObjectOutputStream oos;
		FileInputStream fis;
		ObjectInputStream ois;
		Object instance_g;
		Object instance_s;
		//使用反序列化测试各种单例模式的安全性
		//饿汉式
		System.out.println("========= 饿汉式  =========");
		instance_g = SinglePatterns01.getInstance();
		fos = new FileOutputStream("d:/test01.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test01.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns01)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
		
		//懒汉式
		System.out.println("========= 懒汉式  =========");
		instance_g = SinglePatterns02.getInstance();
		fos = new FileOutputStream("d:/test02.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test02.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns02)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
		
		//双重检测
		System.out.println("========= 双重检测  =========");
		instance_g = SinglePatterns03.getInstance();
		fos = new FileOutputStream("d:/test03.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test03.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns03)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
		
		//静态内部类式
		System.out.println("========= 静态内部类式  =========");
		instance_g = SinglePatterns04.getInstance();
		fos = new FileOutputStream("d:/test04.txt");
		oos = new ObjectOutputStream(fos);
		oos.writeObject(instance_g);
		
		fis = new FileInputStream("d:/test04.txt");
		ois = new ObjectInputStream(fis);
		instance_s = (SinglePatterns04)ois.readObject();
		
		System.out.println(instance_g);
		System.out.println(instance_s);
	}
}

8.2 测试结果

========= 饿汉式  =========
ft.singlePatterns.SinglePatterns01@1d56ce6a
ft.singlePatterns.SinglePatterns01@7a07c5b4
========= 懒汉式  =========
ft.singlePatterns.SinglePatterns02@2ef1e4fa
ft.singlePatterns.SinglePatterns02@306a30c7
========= 双重检测  =========
ft.singlePatterns.SinglePatterns03@421faab1
ft.singlePatterns.SinglePatterns03@2b71fc7e
========= 静态内部类式  =========
ft.singlePatterns.SinglePatterns04@69d0a921
ft.singlePatterns.SinglePatterns04@446cdf90

8.3 改进方法

   反序列化创建对象时,会调用Serializable类的成员函数readResolve(),所以各个实例模式需要重写readResolve()方法,如下所示:

private Object readResolve() throws ObjectStreamException{
  	return instance;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
序列化和反序列化可以破坏单例设计模式的安全性。当一个单例类被序列化后,然后再进行反序列化,会创建出一个新的实例,从而破坏了单例的特性。这是因为序列化和反序列化过程中会创建一个新的对象,并不会调用类的构造函数来初始化新对象。因此,即使单例类被序列化和反序列化,也不能保证只有一个实例存在。 为了解决这个问题,可以在单例类中添加一个readResolve方法,并在该方法中返回单例实例。这样,在反序列化时,就可以通过readResolve方法返回已存在的单例实例,而不是创建一个新的实例。通过这种方式,可以确保单例模式的安全性,避免了序列化和反序列化破坏单例的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [深入浅出单例模式反射与序列化对单例的破坏](https://blog.csdn.net/weixin_43975523/article/details/103140654)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [设计模式|序列化、反序列化单例的破坏、原因分析、解决方案及解析](https://blog.csdn.net/leo187/article/details/104332138)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值