单例模式实现以及防止反射与序列化

单例模式

模式动机

对于系统中的某些类来说,只有一个实例很重要,如一个系统中只有一个计时工具和 ID(序号)生成器。

单例模式适用情况包括:系统只需要一个实例对象;客户调用类的单个实例只允许使用一个公共访问点。

定义

顾名思义,用来保证一个对象只能创建一个实例,除此之外,它还提供了对实例的全局访问方法。

单例模式的要点有三个:一是类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

实现

为了确保单例实例的唯一性,所有的 单例构造器都要被声明为私有 的,再通过声明 静态方法实现全局访问获得 该单例实例。

懒汉式
public class Singleton {
  private static Singleton instance;
  private Singleton(){}

  public static Singleton getInstance(){
    if(instance == null)
      instance = new Singleton();
    return instance;
  }
    
  public void doAction(){
    //TODO 实现你需要做的事
  }
}

懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。有线程安全和线程不安全两种写法,区别就是 synchronized 关键字。

饿汉式
public class Singleton {
  private static Singleton instance = new Singleton();
  private Singleton(){}

  public static Singleton getInstance(){
    return instance;
  }
    
  public void doAction(){
    //TODO 实现你需要做的事
  }
}

饿汉式,从名字上也很好理解,就是“比较勤”,实例在 初始化 的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是 没有线程安全的问题 (利用类加载机制避免了多线程同步问题),坏处是浪费内存空间。

双重校验锁(Double Check)(推荐)
public class Singleton {
  private volatile static Singleton instance = null;
  private Singleton(){}

  public static Singleton getInstance(){
    if(instance == null)
      synchronized (Singleton.class){
        if(instance == null)
          instance = new Singleton();
      }
    return instance;
  }
    
  public void doAction(){
    //TODO 实现你需要做的事
  }
}

优点:

  1. 线程安全
  2. 实现 Lazy loading
  3. 效率较高
原理:

1、第一个 if 校验是为了 提高代码执行效率,由于单例模式只需要创建一次实例即可,所以当实例创建后,再次调用 getInstance 方法就不用再竞争所进入同步代码块,直接返回前面创建好的实例即可。

2、第二个 if 校验是为了 防止二次创建实例。由于第一次 if 校验没有同步,有可能多个线程都进入到了第一个 if 里面竞争资源,假如没有第二次校验, t1,t2 都在竞争同步资源,t2 获取到资源后,创建实例,然后资源释放,t1 获取到资源, t1 就也会创建一个实例,那么,就会出现创建多个实例的情况,所以,第二次 if 校验可以 完全避免多线程导致创建多次实例的问题

private volatile static Singleton instance = null; 这里的 volatile 必不可少,volatile 关键字可以避免 JVM 指令重排优化。

因为 instance = new Singleton(); 可以拆分为三步:

  1. 为 singleton 分配内存空间;
  2. 初始化 singleton;
  3. 将 singleton 指向分配的内存空间;

但由于 JVM 具有指令重排序的特性,执行顺序可能变为 1-3-2。指令重排序在单线程下不会出现问题,但是在 多线程下会导致线程获取一个未初始化的实例。例如:线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 singleton 不为空,因此返回 singleton,但是此时的 singleton 还没有被初始化。
使用 volatile 会禁止 JVM 指令重排,从而保证在多线程下也能正常执行。

静态内部类(推荐)
public class Singleton {
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
    private Singleton() {
        
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
    
    public void doAction(){
    	//TODO 实现你需要做的事
  	}
}

这种方式利用了类装载机制来保证初始化实例时只有一个线程,静态内部类在 Singleton 被装载时并不会立即实例化,而是在调用 getInstance() 时才会装载静态内部类,从而完成 Singleton 实例化。由于类的静态属性只会在第一次加载类的时候进行初始化,就通过 JVM 加载类时的线程安全的特性来保证了线程安全。可能还存在反射攻击或者反序列化攻击。

优点:

  1. 利用 JVM 加载静态内部类的机制保证多线程安全
  2. 实现 Lazy loading 效果
  3. 效率高
枚举实现(推荐)
public enum Singleton {
    INSTANCE;
    public void doSomething() {
        System.out.println("doSomething");
    }
}

自动支持序列化机制,绝对防止多次实例化。

优点:

  1. 线程安全(枚举实例的创建默认就是线程安全的)
  2. 防止反射,克隆及序列化对单例的破坏
破环单例模式的三种方式:反射,序列化,克隆

单例模式的实现方式及如何有效防止防止反射和反序列化 - 叫我鹏爷 - 博客园 (cnblogs.com)

以 Double Check 为例子,测试反射,序列化,克隆是否能破环单例模式:

public class Singleton implements Serializable,Cloneable {
    private static volatile Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试用例:

public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        System.out.println("原本的 singleton 的 hashcode: " + instance.hashCode());
        //反射
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println("反射获取的 singleton 的 hashcode: " + singleton.hashCode());
        
        //克隆
        Singleton clone = (Singleton) Singleton.getInstance().clone();
        System.out.println("克隆获取的 singleton 的 hashcode: " + clone.hashCode());

        //序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
    	ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(Singleton.getInstance());
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        Singleton serialize = (Singleton) ois.readObject();
        //关闭资源略
        System.out.println("序列化获取的 singleton 的 hashCode: "+ serialize.hashCode());
    }

输出结果:

原本的 singleton 的 hashcode: 460141958
反射获取的 singleton 的 hashcode: 1163157884
克隆获取的 singleton 的 hashcode: 1956725890
序列化获取的 singleton 的 hashCode: 666641942

运行结果表明通过 getInstance()、反射、克隆、序列化这四种方式得到的 Singleton 对象的 hashCode 是不一样的,此时单例模式已然被破环

如何防止反射、克隆、序列化对单例模式的破环

1、防止反射破环(虽然构造方法已私有化,但通过反射机制使用 newInstance() 方法构造方法也是可以被调用):

  • 首先定义一个全局变量开关 isFristCreate 默认为开启状态
  • 当第一次加载时将其状态更改为关闭状态

2、防止克隆破环

  • 重写 clone(),直接返回单例对象

3、防止序列化破环

  • 添加 readResolve(),返回 Object 对象
public class Singleton implements Serializable, Cloneable {
    private static final long serialVersionUID = 6125990676610180062L;
    private volatile static Singleton singleton;
    private static boolean isFristCreate = true;//默认是第一次创建

    private Singleton() {
        if (isFristCreate) {
            synchronized (Singleton.class) {
                if (isFristCreate) {
                    isFristCreate = false;
                }
            }
        } else {
            throw new RuntimeException("已然被实例化一次,不能再实例化");
        }
    }

    public void doAction() {
        //TODO 实现你需要做的事
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    @Override
    protected Singleton clone() throws CloneNotSupportedException {
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
}

测试用例:

public static void main(String[] args) throws Exception {
        Singleton instance = Singleton.getInstance();
        System.out.println("原本的 singleton 的 hashcode: " + instance.hashCode());
        
        //克隆
        Singleton clone = (Singleton) Singleton.getInstance().clone();
        System.out.println("克隆获取的 singleton 的 hashcode: " + clone.hashCode());

        //序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(Singleton.getInstance());
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        Singleton serialize = (Singleton) ois.readObject();
        //关闭资源略
        System.out.println("序列化获取的 singleton 的 hashCode: "+ serialize.hashCode());
        
        //反射
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println("反射获取的 singleton 的 hashcode: " + singleton.hashCode());
    }

测试结果:

原本的 singleton 的 hashcode: 460141958
克隆获取的 singleton 的 hashcode: 460141958
序列化获取的 singleton 的 hashCode: 460141958
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at pc.TestSingleton.main(TestSingleton.java:37)
Caused by: java.lang.RuntimeException: 已然被实例化一次,不能再实例化
	at pc.Singleton.<init>(Singleton.java:24)
	... 5 more

相关文章

[1] 单例模式的五种写法_absolute_chen的博客-CSDN博客_单例模式

[2] 设计模式(二)单例模式的七种写法_Android进阶三部曲 - 刘望舒-CSDN博客_单例模式写法

[3] 单例模式的实现方式及如何有效防止防止反射和反序列化 - 叫我鹏爷 - 博客园 (cnblogs.com) (推荐)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值