您的位置:首页 > 编程语言 > Java开发

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

2017-04-06 14:37 639 查看
在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

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

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

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

第一种:懒汉式(线程不安全)
/**
* 懒汉式
* 线程不安全
* @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内部实现)。在实际项目中比较少见。

当然还有其他写法,一般单例都是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。

说到单例我会想到两个问题:

单例模式的序列化

单例模式的侵犯

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

解决办法:

序列化单例,重写readResolve()方法
在私有构造器里判断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);

}
}


这次总结让我更加加深了对单例模式,线程安全,反射侵犯以及序列化对单例的影响有了更加深刻的理解。总归一句话,没有强无敌的单例,适合系统才是最好的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  设计模式 java
相关文章推荐