zoukankan      html  css  js  c++  java
  • 单例模式

    单例模式定义:不管在什么场景下只能生成一个类实例;

    单例模式主要注意以下几个问题:

    1. 线程安全问题;解决方法:sychronized、静态内部类、双重检测时用volatile;

    2. 反射破坏单例模式;解决方法:单例构造函数中加对象非空校验;

    3. 序列化反序列化破坏单例模式;解决方法:重写readResolve方法或者用枚举单例;

    懒汉和饿汉的本质区别,就是实例化对象的时机,即是什么时候将对象创建起来

    饿汉式:在加载类的时候就创建单例对象,所以调用时肯定线程安全
    缺点是:不管用不用,都会创建对象,消耗内存对象,可能造成浪费(占着茅坑不拉屎)
    @ThreadSafe
    public class Hungry {
      private Hungry() {}
      private static final Hungry instance = new Hungry();
      public static Hungry getInstance() { return instance; } 
    }

    懒汉式单例模式建议使用静态内部类,JVM能够确保线程安全问题,但是要解决反射攻击和序列化反序列化攻击,还得通过代码来实现:

    解决反射攻击的问题:需要在构造函数中控制私有静态成员变量的重复创建;

    解决序列化反序列化攻击的问题:重写readResolve()方法

    /**
     * Created by marcopan on 2018/9/14.
     * 有反射攻击和序列化攻击的问题
     *
     * 反射攻击解决方法,在构造函数中判断单例是否为空
     */
    public class LazySingleton implements Serializable {
        private LazySingleton() {
            // 防止通过反射来破坏单例
            if (LazySingletonHolder.singleton != null) {
                throw new RuntimeException("不允许创建多个实例");
            }
        }
    
        public static final LazySingleton getInstance() {
            return LazySingletonHolder.singleton;
        }
    
        private static class LazySingletonHolder {
            private static final LazySingleton singleton = new LazySingleton();
        }
    
    //    public static void main(String[] args) {
    //        try {
    //            //很无聊的情况下,进行破坏
    //            Class<?> clazz = LazySingleton.class;
    //            //通过反射拿到私有的构造方法
    //            Constructor c = clazz.getDeclaredConstructor(null);
    //            //强制访问,强吻,不愿意也要吻
    //            c.setAccessible(true);
    //
    //            //暴力初始化
    //            Object o1 = c.newInstance();
    //            //调用了两次构造方法,相当于new了两次,犯了原则性问题,
    //            Object o2 = LazySingleton.getInstance();//c.newInstance();
    //
    //            System.out.println(o1 == o2);
    //        } catch (Exception e) {
    //            e.printStackTrace();
    //        }
    //    }
        
        private Object readResolve() {
            return LazySingletonHolder.singleton;
        }
    
        public static void main(String[] args) {
            try {
                LazySingleton instance1 = LazySingleton.getInstance();
    
                FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(instance1);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("EnumSingleton.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                LazySingleton instance2 = (LazySingleton) ois.readObject();
                ois.close();
    
                System.out.println(instance1 == instance2);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    双重校验机制需要添加volatile关键字,防止指令重排序,双重校验机制同样无法解决反射攻击和序列化反序列化的问题。

    public class RegisterSingleton {
        /** volatile防止指令重排序
         *  有反射和序列化攻击的问题
         */
        private volatile static RegisterSingleton lazy = null;
    
        private RegisterSingleton() {
        }
    
        public static RegisterSingleton getInstance() {
            if (lazy == null) {
                synchronized (RegisterSingleton.class) {
                    if (lazy == null) {
                        lazy = new RegisterSingleton();
                        //1.分配内存给这个对象
                        //2.初始化对象
                        //3.设置lazy指向刚分配的内存地址
                        //4.初次访问对象
                    }
                }
            }
            return lazy;
        }
    }
    

    枚举单例模式能够完美解决反射攻击和序列化反序列化的问题。

    public enum EnumSingleton {
    
        INSTANCE;
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    
        public static EnumSingleton getInstance() {
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            try {
                EnumSingleton instance1 = EnumSingleton.getInstance();
                instance1.setData(new Object());
    
                FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(instance1);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("EnumSingleton.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                EnumSingleton instance2 = (EnumSingleton) ois.readObject();
                ois.close();
    
                System.out.println(instance1.getData());
                System.out.println(instance2.getData());
                System.out.println(instance1.getData() == instance2.getData());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    其中关于枚举的可以参考这篇文章,写得挺透彻的http://www.manongjc.com/article/1597.html

    这里还要强调一点关于双重检查锁的情况,双重检查锁之所以是线程不安全的,原因在于CPU处理器会对指令进行重排序,有线程可能会拿到没有完成初始化的对象。双重检查锁要使用volatile变量避免双重检查锁线程不安全的问题

  • 相关阅读:
    实用产品规划
    产品经理对用户的调研
    产品经理用户研究
    竞品分析方案
    产品竞品分析
    Mybatis Plus
    shiro
    Spring cloud
    Spring Boot
    Redis入门(二)
  • 原文地址:https://www.cnblogs.com/panning/p/7668881.html
Copyright © 2011-2022 走看看