Java单例模式与反射及序列化及枚举实现单例

2018-09-1423:04:05后端程序开发Comments1,573 views字数 4060阅读模式

单例模式与反射

单例模式最根本的在于类只能有一个实例,如果通过反射来构建这个类的实例,单例模式就会被破坏,下面我们通过例子来看下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

/**
 * 静态内部类式单例模式
 */
class Singleton implements Serializable{
	
	private static class SingletonClassInstance {
	    private static final Singleton instance = new Singleton();
	}
	
	//方法没有同步,调用效率高
	public static Singleton getInstance() {
	    return SingletonClassInstance.instance;
	}
	
	private Singleton() {}
}
复制代码

相信大家对于这个单例的这种实现方式肯定不陌生,下面我们来看看通过反射来创建类实例会不会破坏单例模式。main函数代码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

Singleton sc1 = Singleton.getInstance();
Singleton sc2 = Singleton.getInstance();
System.out.println(sc1); // sc1,sc2是同一个对象
System.out.println(sc2);

/*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/
Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.learn.example.Singleton");
Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳过权限检查
Singleton sc3 = c.newInstance();
Singleton sc4 = c.newInstance();
System.out.println("通过反射的方式获取的对象sc3:" + sc3);  // sc3,sc4不是同一个对象
System.out.println("通过反射的方式获取的对象sc4:" + sc4);
复制代码

下面我们来看输出:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
通过反射的方式获取的对象sc3:com.learn.example.Singleton@25154f
通过反射的方式获取的对象sc4:com.learn.example.Singleton@10dea4e
复制代码

我们看到正常的调用getInstance是符合我们预期的,如果通过反射(绕过检查,通过反射可以调用私有的),那么单例模式其实是失效了,我们创建了两个完全不同的对象sc3和sc4。我们如何来修复这个问题呢?反射需要调用构造函数,那我们可以在构造函数里面进行判断。修复代码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

class Singleton implements Serializable{
	
    private static class SingletonClassInstance {
    	private static final Singleton instance = new Singleton();
    }
    
    //方法没有同步,调用效率高
    public static Singleton getInstance() {
    	return SingletonClassInstance.instance;
    }
    
    //防止反射获取多个对象的漏洞
    private Singleton() {
    	if (null != SingletonClassInstance.instance)
    	    throw new RuntimeException();
    }
}
复制代码

我们看到唯一的改进在于,构造函数里面添加了判断,如果当前已有实例,通过抛出异常来阻止反射创建对象。我们来看下输出:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at com.learn.example.RunMain.main(RunMain.java:45)
Caused by: java.lang.RuntimeException
	at com.learn.example.Singleton.<init>(RunMain.java:28)
	... 5 more
复制代码

我们看到,我们通过反射创建对象的时候会抛出异常了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

单例模式与序列化

除了反射以外,反序列化过程也会破坏单例模式,我们来看下现阶段反序列化输出的结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@16ec8df
复制代码

我们看到反序列化后的对象和原对象sc1已经不是同一个对象了。我们需要对反序列化过程进行处理,处理代码如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

//防止反序列化获取多个对象的漏洞。
//无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
//实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象
private Object readResolve() throws ObjectStreamException {  
    return SingletonClassInstance.instance;
}
复制代码

我们从注释里面也可以看出来,readResolve方法会将原来反序列化出来的对象进行覆盖。我们丢弃原来反序列化出来的对象,使用已经创建的好的单例对象进行覆盖。我们来看现在的输出:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@52e922
复制代码

使用枚举实现单例

Effective Java中推荐使用枚举来实现单例,因为枚举实现单例可以阻止反射及序列化的漏洞,下面我们通过例子来看下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

class Resource{}

/**
 * 使用枚举实现单例
 */
enum SingletonEnum{
    INSTANCE;
    
    private Resource instance;
    SingletonEnum() {
        instance = new Resource();
    }
    public Resource getInstance() {
        return instance;
    }
}
复制代码

我们在main方法中调用代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

Resource resource1 = SingletonEnum.INSTANCE.getInstance();
Resource resource2 = SingletonEnum.INSTANCE.getInstance();
System.out.println(resource1);
System.out.println(resource2);
复制代码

输出如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

com.learn.example.Resource@52e922
com.learn.example.Resource@52e922
复制代码

我们看到,通过枚举我们实现了单例,那么枚举是如何保证单例的(如何满足多线程及序列化的标准的)?其实枚举是一个普通的类,它继承自java.lang.Enum类。我们将上面的class文件反编译后,会得到如下代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

public final class SingletonEnum extends Enum<SingletonEnum> {
    public static final SingletonEnum INSTANCE;
    public static SingletonEnum[] values();
    public static SingletonEnum valueOf(String s);
    static {};
}
复制代码

由反编译后的代码可知,INSTANCE 被声明为static 的,在类加载过程,可以知道虚拟机会保证一个类的() 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

枚举实现与序列化

Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,以下面枚举为例,序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
Effective Java中单元素的枚举类型被作者认为是实现Singleton的最佳方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/bc/4636.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/bc/4636.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定