设计模式——单例模式(防止序列化以及反射机制侵犯)

本文探讨了Java中单例模式的重要性和实现方式,包括私有构造器、静态变量以及公共访问方法。同时,针对单例模式可能遇到的序列化和反射攻击问题,提出了通过重写readResolve()方法以及在构造器中检查实例的存在来防止此类侵犯的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

  1. 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
  2. 省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
  3. 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

创建单例很简单,总共三点:

  1. 私有构造器
  2. 声明一个私有的静态变量
  3. 提供一个对外的公共的静态方法访问该变量,如果该变量没有对象,则创建该对象
但是单例有很多种,至于哪一种才是天下无敌的,那要看你系统需要哪种,只有适合才是最好的。好了上代码。

第一种:懒汉式(线程不安全)
/**
 * 懒汉式
 * 线程不安全
 * @author Hacfqx
 *
 */
public class SingletonDemo1{
	//声明一个私有的静态变量
	private static SingletonDemo1 instance = null;
	//私有构造器
	private SingletonDemo1(){}
	//提供一个对外的公共的静态方法访问该变量,如果该变量没有对象,则创建该对象
	public static SingletonDemo1 getInstance(){
		if (null == instance) {
			instance = new SingletonDemo1();
		}
		return instance;
	}
}


在单线程模式下可以使用这个模式创建,但是多线程场景下就万万不可了,该单例模式是线程不安全的,当系统创建的时候回出现多个instance。

第二种:懒汉式(线程安全)
/**
 * 懒汉式
 * 线程安全
 * @author Hacfqx
 *
 */
public class SingletonDemo2{
	
	//声明一个私有的静态变量
	private static SingletonDemo2 instance = null;
	
	//私有构造器
	private SingletonDemo2(){}
	
	//此处增加同步锁synchronized
	public static synchronized SingletonDemo2 getInstance(){
		if (null == instance) {
			instance = new SingletonDemo2();
		}
		return instance;
	}
}
该写法在多线程环境能很好的工作,但是效率很低每次获取单例的时候都要先获取锁,这时候性能极其低下。不建议这样做


第三种:饿汉式  (我好饿,我不管,我就要先拥有这个instance,线程安全但是效率比较低)
public class SingletonDemo3 {   
	 private static SingletonDemo3 instance = new SingletonDemo3();
	 private SingletonDemo3() {}
	 public static SingletonDemo3 getInstance(){
	    return instance;
	 }
}

这种方式基于 classloder 机制避免了多线程的同步问题,不过, instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法,   但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
第四种:双重校验锁
/**
 * synchronized锁
 * 
 * @author Hacfqx
 *
 */
public class SingletonDemo4 {
	private static SingletonDemo4 instance = null;

	private SingletonDemo4() {
	}

	/**
	 * 双重校验
	 * 将synchronized关键字加在了内部,
	 * 也就是说当调用的时候是不需要加锁的,
	 * 只有在instance为null,并创建对象的时候才需要加锁,
	 * 性能有一定的提升。
	 * 但是,这样的情况,还是有可能有问题的,
	 * 看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,
	 * 也就是说instance = new Singleton();语句是分两步执行的。
	 * 但是JVM并不保证这两个操作的先后顺序,
	 * 也就是说有可能JVM会为新的Singleton实例分配空间,
	 * 然后直接赋值给instance成员,然后再去初始化这个Singleton实例。
	 * 这样就可能出错了
	 * @return
	 */
	public static SingletonDemo4 getInstance() {
		if (null == instance) {
			synchronized (instance) {
				if (null == instance) {
					instance = new SingletonDemo4();
				}
			}
		}
		return instance;
	}
}

第五种:静态内部类
/**
 * 单例模式使用内部类来维护单例的实现,
 * JVM内部的机制能够保证当一个类被加载的时候,
 * 这个类的加载过程是线程互斥的。
 * 这样当我们第一次调用getInstance的时候,
 * JVM能够帮我们保证instance只被创建一次,
 * 并且会保证把赋值给instance的内存初始化完毕,
 * 这样我们就不用担心上面的问题。
 * 同时该方法也只会在第一次调用的时候使用互斥机制,
 * 这样就解决了低性能问题。
 * 
 * @author Hacfqx
 *
 */

public class SingletonDemo4 {

	private SingletonDemo4() {
	}

	/**
	 * 静态内部类来维护单例
	 * @return
	 */
	private static class SingeletonHolder{
		private final static SingletonDemo4 instance = new SingletonDemo4();
	}
	
	/**
	 * 获取实例
	 * @return
	 */
	public static SingletonDemo4 getInstance(){
		return SingletonHolder.instance();
	}
}

第六种:枚举类单例
public enum EnumSingleton {
	INSTANCE;
	
	public void method(){
		//TODO
	}
}
该写法不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象(具体可以查看enum内部实现)在实际项目中比较少见。

当然还有其他写法, 一般单例都是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。
说到单例我会想到两个问题:
  1. 单例模式的序列化
  2. 单例模式的侵犯

下面是解决代码(我随便拿一个单例举例):

解决办法:

  1. 序列化单例,重写readResolve()方法
  2. 在私有构造器里判断intance,如存在则抛异常(防止反射侵犯私有构造器)
我把这两个放在同一个类里执行验证,所以下面需要各位亲手运行代码感受了。= =会有收获的。

public class SingletonDemo6 implements Serializable{  
      
    // 类初始化时,不初始化这个对象(延迟加载,真正用的时候再创建)  
    private static SingletonDemo6 instance;  
      
    private SingletonDemo6() {  
        // 防止反射获取多个对象的漏洞  
        if (null != instance) {  
            throw new RuntimeException("单例模式被侵犯!");  
        }  
    }  
      
    public static synchronized SingletonDemo6 getInstance() {  
        if (null == instance)  
            instance = new SingletonDemo6();  
        return instance;  
    }  
  
    // 防止反序列化获取多个对象的漏洞。  
    // 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。  
    // 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。  
    private Object readResolve() throws ObjectStreamException {    
        return instance;  
    }  
} 

package com.zz.designpatterns.createpattern.singletonpattern;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;

public class ReflectAndSerialSingleton {
		public static void main(String[] args) throws Exception {  
	        SingletonDemo6 sc1 = SingletonDemo6.getInstance();  
	        SingletonDemo6 sc2 = SingletonDemo6.getInstance();    
	        System.out.println(sc1);  
	        System.out.println(sc2);
	        System.out.println(sc1.equals(sc2)); // sc1,sc2是同一个对象  
	         
	     	/**
	     	 * 通过反序列化的方式构造多个对象(类需要实现Serializable接口)  
	      	*/
	        // 1. 把对象sc1写入硬盘文件  
	        FileOutputStream fos = new FileOutputStream("object.txt");  
	        ObjectOutputStream oos = new ObjectOutputStream(fos);  
	        oos.writeObject(sc1);  
	        oos.close();  
	        fos.close();  
	          
	        // 2. 把硬盘文件上的对象读出来  
	        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));  
	        // 如果对象定义了readResolve()方法,readObject()会调用readResolve()方法。从而解决反序列化的漏洞  
	        SingletonDemo6 sc5 = (SingletonDemo6) ois.readObject();  
	        // 反序列化出来的对象,和原对象,不是同一个对象。如果对象定义了readResolve()方法,可以解决此问题。  
	        System.out.println(sc5);   
	        ois.close(); 
	        
	        /**
	         * 通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)  
	         */
	        Class<SingletonDemo6> clazz = SingletonDemo6.class; 
	        Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null); 
	        c.setAccessible(true); // 跳过权限检查 
	        SingletonDemo6 sc3 = c.newInstance(); 
	        SingletonDemo6 sc4 = c.newInstance(); 
	        System.out.println(sc3);  // sc3,sc4不是同一个对象 
	        System.out.println(sc4);
	          
	         
	}
}

这次总结让我更加加深了对单例模式,线程安全,反射侵犯以及序列化对单例的影响有了更加深刻的理解。总归一句话,没有强无敌的单例,适合系统才是最好的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值