• 单例模式(Singleton Pattern)


    一、单例模式的经典实现方式  

      单例模式分为饿汉式(立即加载)和懒汉式(延迟加载),其中懒汉式又可以分为双重检查锁、静态内部类和枚举三种情况。;

      单例模式使用不当,则会产生线程安全问题:

        饿汉式不会产生线程安全问题,但是它一般不使用,因为他会浪费内存空间;

        懒汉式会合理的使用内存空间,因为只有第一次被加载的时候才会真正的创建对象;但是这种方式存在线程安全问题

      创建单例模式的三要素:私有静态成员变量(对象本身)、私有的构造函数、公共的获取静态变量方法

    1、饿汉式

      类加载的时候,JVM内部保证了整个过程的线程安全。类加载包括静态成员变量的初始化。

      所以饿汉式在类加载时,就会加载静态变量,从而对静态属性进行初始化,这个操作是受JVM线程保护的,不会出现线程安全问题。

    package com.lcl.galaxy.design.pattern.singleton;
    
    public class HungrySingleAnimal {
        private static HungrySingleAnimal singleAnimal = new HungrySingleAnimal();
    
        private HungrySingleAnimal(){
    
        }
    
        public static HungrySingleAnimal getSingleAnimal(){
            return singleAnimal;
        }
    
    }

    2、懒汉式

      懒汉式的最优写法就是使用双重检查锁的方式实现,实现代码如下:

    package com.lcl.galaxy.design.pattern.singleton;
    
    import java.io.Serializable;
    
    public class DoubleCheckSingleAnimal implements Serializable {private static volatile DoubleCheckSingleAnimal singleAnimal = null;
    
        private DoubleCheckSingleAnimal(){
    
        }
    
        public static DoubleCheckSingleAnimal getSingleAnima (){
            if(singleAnimal == null){
                synchronized (DoubleCheckSingleAnimal.class){
                    if(singleAnimal == null){
                        singleAnimal = new DoubleCheckSingleAnimal();
                    }
                }
            }
            return singleAnimal;
        }
    
        private Object readResolve(){
            return singleAnimal;
        }
    }

      为什么要这么实现呢,我们可以一步步的分析

      最简单的实现方式:

        private static volatile LazySingleAnimal1 singleAnimal = null;
    
        private LazySingleAnimal1(){
    
        }
    
        /**
         * 不安全
         * @return
         */
        public static LazySingleAnimal1 getSingleAnimal(){
            if(singleAnimal == null){
                singleAnimal = new LazySingleAnimal1();
            }
            return singleAnimal;
        }

      这种实现方式会存在线程安全问题:当存在并发时,如果对象还未创建,则都会执行创建对象语句。

      针对上述问题的优化如下

        public static synchronized LazySingleAnimal1 getSingleAnima2(){
            if(singleAnimal == null){
                singleAnimal = new LazySingleAnimal1();
            }
            return singleAnimal;
        }

      优化方式是将方法增加一个synchronized同步锁,这样的问题就在于,会非常影响单例的使用性能

      针对上述内容进一步优化

        public static LazySingleAnimal1 getSingleAnima3 (){
            if(singleAnimal == null){
                synchronized (LazySingleAnimal1.class){
                    singleAnimal = new LazySingleAnimal1();
                }
            }
            return singleAnimal;
        }

      这种实现,只有在对象为空的时候才加锁,但是仍然存在问题:即如果存在并发,对象为空时,都能绕过为空判断,虽然在创建对象语句的执行上有锁,但是仍然会以串行方式创建多个对象。

      继续上述问题优化

        public static LazySingleAnimal1 getSingleAnima4 (){
            if(singleAnimal == null){
                synchronized (LazySingleAnimal1.class){
                    if(singleAnimal == null){
                        singleAnimal = new LazySingleAnimal1();
                    }
                }
            }
            return singleAnimal;
        }

      这种情况已经控制住了,只有一个线程可以进入第二层为空判断,且创建了对象,释放锁后,后续竞争锁成功的线程判断时,对象已不为空,那么就不会在创建对象;但是该种实现仍然存在问题:JVM会对代码进行指令重排序,因此可能存在一个对象虽然已经创建,但是还未赋值的情况下,就被其他线程所使用,进而导致错误产生。因此需要对私有属性加volatile修饰。

      这里需要说明一个指令重排序和volatile关键字的作用:

        对象的创建流程:1、new关键字开辟空间;2、对象空间初始化;3、将内存地址赋值给栈空间的变量进行保存

        指令重排序:JIT即时编译器,会对指令做重排序,上述创建顺序有可能不是按照123执行的,而是按照132执行的,那么就有个问题,先将引用对象做了保存,但是没有对对象的空间做初始化,那么下一个线程拿到的引用就会有问题

        并发编程的三大特性:1、原子性:狭义上指CPU指令的原子操作,广义上指字节码指令的原子性;2、有序性:CPU指令有序性;3、可见性:CPU工作内存种的数据存在多核之间的不可见问题

        volatile作用:解决有序性问题(禁止指令重排);解决可见性问题(强制刷新告诉缓存到内存中,当多个CPU对同一个数据进行操作时,一旦有数据有写操作,那么必须先等它写完数据之后,写入主内存)

      这里还有一个问题,就是单例模式有可能被破坏,被破坏主要有反射破坏和序列化破坏,再最优的单例模式中增加了一个readResolve方法,就是为了防止使用序列化破坏。

    3、静态内部类

      静态内部类,主要是利用了静态类加载受JVM保护,在调用公共的get方法时,会去访问静态内部类的私有属性,此使就会触发静态内部类的加载,那么就初始化了静态内部类中的私有属性,同时又因为静态类的加载受JVM保护,因此静态内部类也是一个很好的单例实现方式。

    package com.lcl.galaxy.design.pattern.singleton;
    
    public class StaticInnerClass {
    
        private static class SingletonHandler{
            private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass();
        }
    
        private StaticInnerClass(){}
    
        public StaticInnerClass getStaticInnerClass(){
            return SingletonHandler.STATIC_INNER_CLASS;
        }
    }

    4、枚举

      先上代码,具体原因后面说。

    package com.lcl.galaxy.design.pattern.singleton;
    
    public enum EnumSingleton {
        INSTANCE;
    
        public EnumSingleton getInstance(){
            return INSTANCE;
        }
    }

    二、破坏单例

    1、反射攻击

      使用反射获取对象的无参构造,然后使用构造函数的newInstance方法进行创建对象。

        @Test
        public void reflectAttackTest() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
            Constructor constructor = DoubleCheckSingleAnimal.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            DoubleCheckSingleAnimal a1 = (DoubleCheckSingleAnimal) constructor.newInstance();
            DoubleCheckSingleAnimal a2 = (DoubleCheckSingleAnimal) constructor.newInstance();
    
            a1.setName("lcl");
            a2.setName("lcl");
            log.info("a1============={}==========",a1);
            log.info("a2============={}==========",a2);
            log.info("a1.equals(a2)============={}==========",a1.equals(a2));
        }

    2、序列化攻击

      使用深拷贝的方式,创建两个对象。其实就是先使用序列化,将对象写到一个文件内,然后再将文件中的内容反序列化为对象。

        @Test
        public void serializationAttackTest() throws IOException, ClassNotFoundException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("aaa"));
            DoubleCheckSingleAnimal a1 = DoubleCheckSingleAnimal.getSingleAnima();
            oos.writeObject(a1);
    
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("aaa"));
            DoubleCheckSingleAnimal a2 = (DoubleCheckSingleAnimal) ois.readObject();
    
            a1.setName("lcl");
            a2.setName("lcl");
            log.info("a1============={}==========",a1);
            log.info("a2============={}==========",a2);
            log.info("a1.equals(a2)============={}==========",a1.equals(a2));
        }

     三、攻击防御

    1、反射攻击防御

      对于反射攻击,private属性简直形同虚设,同时反射是基于对象的构造函数进行创建对象的,那么就可以在构造函数中加入判断,如果对象不为空,则抛出异常

    package com.lcl.galaxy.design.pattern.singleton;
    
    public class StaticInnerClass {
    
        private static class SingletonHandler{
            private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass();
        }
    
        private StaticInnerClass(){}
    
        public StaticInnerClass getStaticInnerClass() throws Exception {
            if(SingletonHandler.STATIC_INNER_CLASS != null){
                throw new Exception("单例被破坏");
            }
            return SingletonHandler.STATIC_INNER_CLASS;
        }
    }

    2、反序列化攻击防御

      这里可以看一下序列化时调用的ois.readObject方法,在该方法的源码中,会判断是否存在readResolve方法,如果存在,则调用该方法进行处理,如果不存在,则反序列化对象,因此,在上面提到,双重检查锁的单例模式,需要加入readResolve方法,这样就可以避免通过序列化攻击单例的情况(上面代码示例的readResolve方法)。

    package com.lcl.galaxy.design.pattern.singleton;
    
    import java.io.Serializable;
    
    public class DoubleCheckSingleAnimal implements Serializable {
    
        private static volatile DoubleCheckSingleAnimal singleAnimal = null;
    
        private DoubleCheckSingleAnimal() throws Exception {
        }
    
        public static DoubleCheckSingleAnimal getSingleAnima () throws Exception {
            if(singleAnimal == null){
                synchronized (DoubleCheckSingleAnimal.class){
                    if(singleAnimal == null){
                        singleAnimal = new DoubleCheckSingleAnimal();
                    }
                }
            }
            return singleAnimal;
        }
    
        private Object readResolve(){
            return singleAnimal;
        }
    
    }

    3、枚举单例防御

      JVM对枚举类做了特殊的处理,既保证了线程安全,又防止了序列化攻击,同时也防止了反射攻击。

    public abstract class Singleton extends Enum

      保证线程安全:查看枚举类编译后的文件(如上述代码)可以发现,枚举类是用static修饰的类;JVM在加载static类的时候,会保证线程安全

      防止序列化攻击:JVM对枚举类做了特殊处理,在序列化时,只保存了属性名称和引用地址,当反序列化时,使用的是引用地址和名称进行处理,因此拿到的对象仍然是同一个对象。

      防止反射攻击:枚举编译后的文件是抽象类,因此不能使用反射进行破坏。

  • 相关阅读:
    NFS挂载报错
    关于git的reset指令说明-soft、mixed、hard
    nginx关于限制请求数和连接数
    Jenkins初级使用过程中的异常处理(1)
    Jenkins的初级应用(2)-Invoke Phing targets
    Jenkins的初级应用(1)-Publish Over SSH
    解决Jenkins安装的时区问题
    用Ubuntu快速安装Jenkins
    TIME_WAIT状态过多的排查
    在linux环境下用中文查询数据库
  • 原文地址:https://www.cnblogs.com/liconglong/p/14174496.html
Copyright © 2020-2023  润新知