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


    单例模式

    模式动机

    对于系统中的某些类来说,只有一个实例很重要,如一个系统中只有一个计时工具和 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 实现你需要做的事
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    饿汉式
    public class Singleton {
      private static Singleton instance = new Singleton();
      private Singleton(){}
    
      public static Singleton getInstance(){
        return instance;
      }
        
      public void doAction(){
        //TODO 实现你需要做的事
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    双重校验锁(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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    优点:

    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 实现你需要做的事
      	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    优点:

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

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

    优点:

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    测试用例:

    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());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    输出结果:

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

    运行结果表明通过 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    测试用例:

    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());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    测试结果:

    原本的 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    相关文章

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

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

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

  • 相关阅读:
    [go学习笔记.第十章.面向对象编程] 10.面向对象的特性-接口
    模板模式【Java设计模式】
    java基于ssm的健身房会员管理系统
    电脑入门:电脑不认新硬盘时该怎么办?
    Mysql索引特性(重要)
    203. Remove Linked List Elements
    电机位置、速度检测方法大合集
    让Python更优雅更易读(第二集)
    Nginx + keepalived 集群搭建
    CAD Exchanger SDK for Win and Linux Crack
  • 原文地址:https://blog.csdn.net/yangmolulu/article/details/123492904