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

    一单例模式

     

    目录结构

    前言

    接下来的系列文章我们会谈设计模式,设计模式不仅仅存在Java开发语言中,而是遍及软件领域且至关重要,是前辈开发总结的经验,一种设计思想,一种架构;在软件开发中,唯一不变的就是需求的变化,开发人员不仅要满足当下的功能需求,还要考虑对后续可能的变化,设计的系统就应有良好的拓展性。在公司接手上一任的代码,继续开发新功能,如果设计的拓展性不好的话,后期开发会很困难,费时费力,还可能对之前的功能有影响,心里也是忐忑不安,同时也给测试人员添加负担,改动点增多,测试范围增大等等,可见设计模式的重要性。

    本文讲述较为简单的单例模式,单例模式要保证系统中对象唯一,这不是获取对象方的责任,是对象提供方保证这个对象在系统中就只能存在一个。如何保证对象的唯一性,就要从创建对象的角度,创建对象可以通过构造方法Clone对象反序列化时创建对象反射四种方式,那么就需要让类内部创建唯一对象,不让外部直接创建,只提供一个方法供外部获取对象。所以单例模式中第一步构造方法私有,不让外部new 对象,其次实现单例模式的类不会实现Cloneable接口,则不支持Clone对象;前2种方式都能避免,主要是反序列化和反射机制容易破坏单例。以下我们来分别讨论单例模式的几种方式和其存在的问题,以及反序列化和反射如何破坏单例,怎样去避免,如何合理设计单例模式?

    创建对象四种方式:

    • 1、构造方法
    • 2、Clone对象
    • 3、反序列化时创建对象
    • 4、反射

    创建单例的常见几种方式:

    • 1、懒汉式
    • 2、饿汉式
    • 3、双检锁
    • 4、静态内部类方式
    • 5、双检锁变式 - CAS自旋锁
    • 6、枚举

    一、懒汉式

    在需要使用的时候,才创建对象(延迟实例化),存在多线程安全问题。

    package designpattern.singleton;
    /**
     * @author zdd
     * 2020/1/10 5:15 下午
     * Description: 懒汉式创建单例
     */
    public class LazyInstantiateTest {
        private  static  LazyInstantiateTest INSTANCE;
        //1、私有构造方法,防止被其他类创建对象
        private LazyInstantiateTest(){};
        //2、对外提供静态公共方法获取单例对象
        public static LazyInstantiateTest getInstance() {
            if(INSTANCE == null) {
                INSTANCE = new LazyInstantiateTest();
            }
            return INSTANCE;
        }
    }

    二、饿汉式

    也称预加载方式,类在加载初始化时就创建单例对象,饿汉抢食般地创建对象,因此以“饿汉”形容,不存在线程安全问题,但是会占用内存,类一被加载进来就实例化对象到堆中,可能很长时间才被使用或者未被使用,如此造成资源浪费。

    package designpattern.singleton;
    import java.io.Serializable;
    
    /**
     * @author zdd
     * 2020/1/10 5:31 下午
     * Description: 饿汉式实现单例
     */
    public class HungryTest implements Serializable {
        private static HungryTest INSTANCE =  new HungryTest();
        private HungryTest() {};
        public static HungryTest getInstance() {
            return INSTANCE;
        }
    }

    三、双检锁

    package designpattern.singleton;
    /**
     * @author zdd
     * 2020/1/10 5:42 下午
     * Description: 双检锁单例
     */
    public class DoubleCheckTest {
        private static DoubleCheckTest INSTANCE;
    
        private DoubleCheckTest() {}
        public static DoubleCheckTest getInstance() {
           //1,第一次判空为了提高程序效率
            if(INSTANCE ==null) {
                //加锁,这里使用的监视器对象是该类的字节码对象
                synchronized (DoubleCheckTest.class){
                    //2、第二次判空是为了解决多线程安全问题
                    if (INSTANCE == null) {
                        INSTANCE = new DoubleCheckTest();
                    }
                }
            }
            return INSTANCE;
        }
    }

    四、静态内部类

    静态内部类借助的是类加载机制,内部类只有在被调用的时候才加载进来,实现延迟创建对象,是饿汉式的改进,既避免了初始化就创建对象占用内存,又能避免懒汉式的线程安全问题。

    package designpattern.singleton;
    
    import java.io.Serializable;
    /**
     * @author zdd
     * 2020/1/10 5:55 下午
     * Description: 静态内部类单例
     */
    public class StaticInnerClassTest {
        //内部类
        private static class InstanceInnerClass {
        private final  static  StaticInnerClassTest 
          INSTANCE =  new StaticInnerClassTest();
        }
        private StaticInnerClassTest(){}
        public static StaticInnerClassTest getInstance() {
           return InstanceInnerClass.INSTANCE;
        }
    }

    五、双检锁变式 - CAS自旋锁

    网上有个面试题

    面试官问:如何在不使用关键字synchronized、Lock锁的情况下,保证线程安全地实现单例模式?

    能够线程安全创建单例,除了枚举外,有静态内部类和双检锁方式,双检锁用了关键字synchronized,静态内部类利用的类加载的机制,底层也是含有加锁操作的。要想实现不用锁,可以参考循环CAS,无阻塞轮询,利用cas自旋锁原理。

    首先写一个自旋锁类

    package designpattern.singleton;
    
    import cas.SpinLockTest;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * @author zdd
     * 2020/1/10 6:59
     * Description: CAS无阻塞自旋锁
     */
    public class CasLock {
        static AtomicReference<Thread> atomicReference = new AtomicReference<>();
    
        public static void lock() {
            Thread currentThread =  Thread.currentThread();
            for (;;) {
                boolean flag =atomicReference.compareAndSet(null,currentThread);
                if(flag) {
                    break;
                }
            }
        }
        public static void unLock() {
            Thread currentThread = Thread.currentThread();
            Thread momeryThread  = atomicReference.get();
            //比较内存中线程对象与当前对象,不相等就抛出异常,防止未获取到锁的线程调用 unlock
            if(currentThread != momeryThread) {
                throw new IllegalMonitorStateException();
            }
            //释放锁
            atomicReference.compareAndSet(currentThread,null);
        }
    }

    实现双检锁变式单例模式

    package designpattern.singleton;
    
    import cas.SpinLockTest;
    /**
     * @author zdd
     * 2020/1/10 6:46 
     * Description: cas实现单例,实际是cas自旋锁,在synchronized阻塞式加锁的改进,无阻塞式加锁
     */
    public class SingletonCasTest {
        private static SingletonCasTest INSTANCE;
        private static  CasLock spinLock = new CasLock();
    
        private SingletonCasTest() {};
        public static SingletonCasTest getInstance() {
            if(INSTANCE == null) {
               spinLock.lock();
               if (INSTANCE == null) {
                   INSTANCE = new SingletonCasTest();
               }
               spinLock.unLock();
            }
            return new SingletonCasTest();
        }
    }

    六、枚举

    枚举类是《Effective Java》书中推荐的实现单例方式,因为其天然的可防止反序列化和反射破解单例的唯一性,保证有且仅有一个对象,

    因太简洁,可读性不强。

    package designpattern.singleton;
    /**
     * @author zdd
     * 2020/1/10 6:43 下午
     * Description:
     */
    public enum  SingletonEnum{
        INSTANCE;
    }

    七、存在的问题

    7.1 线程安全

    一是需要考虑线程安全问题,这是懒汉式存在的问题,为了解决该问题,可以将getInstance() 方法加上synchronized关键字或者在方法内部加同步代码块,或者用Lock锁机制,这样会导致多线程在获取单例对象时线程安全了,但是效率会降低,同步代码块会比同步方法效率更高一些,主要是同步代码块应该尽可能的缩小代码块的包含范围(标准是恰好包括临界区部分),粒度越小,并发度才更高。

    7.2 反序列问题

    二是反序列化问题,在需要将对象序列化与反序列化时,首先让该单例类实现Serializable接口(标志接口,无内容,实现类可序列化),然而存在的问题就是在反序列化时会新创建一个对象,这样就违背了单例模式的对象唯一性。

    将对象先转为字节写入到输入流中(序列化过程),再从输出流中读取字节,再转换为对象 (反序列化)

    代码示例如下:

    package designpattern.singleton;
    import java.io.*;
    /**
     * @author zdd
     * 2020/1/10 7:23 下午
     * Description: 反序列化破坏单例对象唯一性
     */
    public class DeserializableProblemTest {
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
       //先将对象加载到输入流中,在到输出流获取对象,以饿汉式单例为例
            HungryTest hungry1 = HungryTest.getInstance();;
            HungryTest hungry2 = null;
    
            //1,将单例对象写入流中
            ByteArrayOutputStream  ops = new ByteArrayOutputStream();
            ObjectOutputStream  oos = new ObjectOutputStream(ops);
            oos.writeObject(hungry1);
    
            //2,再从流中读出,转换为对象
            ByteArrayInputStream ips=  new ByteArrayInputStream(ops.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ips);
            hungry2 =(HungryTest) ois.readObject();
            //3、判断是否为同一个对象
            System.out.println(hungry1 ==hungry2);
        }
    }

    运行结果: 证明反序列化后又新创建了对象

    false

    解决反序列化问题:在HungryTest类中添加如下方法

     //防止反序列化破坏单例
        private Object readResolve() {
         return INSTANCE;
        }

    再执行运行结果为 true ,证明是同一个对象,未创建新对象。

    为什么添加一个readResolve 方法就可以防止反序列化创建新的对象呢?

    进入ObjectInputStream的 readObject() 可见,下面只列出关键代码位置,详细可自己查看源码

    首先类要支持序列化,通过反射创建新对象赋值给obj

    继续往下看,这里有if判断,满足3个条件,其中hasReadResolveMethod判断是否有readResolve方法,有则调用该方法,最后obj被readResolve返回对象覆盖。

    那么readResolveMethod需要满足什么要求? 满足以下3个条件即可

    参考博客单例模式的攻击之序列化与反序列化

    7.3 反射

    三是反射,我们知道Java中反射几乎是无所不能,你不让我创建对象,那就暴力反射创建,我们如何防止反射破解单例?

    暴力反射破坏单例示例:

    package designpattern.singleton;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    /**
     * @author zdd
     * 2020/1/13 2:49 下午
     * Description:  暴力反射破解单例
     */
    public class ReflectBreakSingletonTest {
    
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            //1,获取单例对象
            HungryTest hungry1 = HungryTest.getInstance();
            //2, 获取HungryTest类字节码对象
            Class<HungryTest> hungryClass=  HungryTest.class;
            //3,获取构造器对象 
            Constructor<HungryTest>  hungryConstructor = hungryClass.getDeclaredConstructor();
            //4,设置暴力反射为true
            hungryConstructor.setAccessible(true);
            //5,通过构造器对象调用默认构造器创建对象 --> 反射 
            HungryTest hungry2=  hungryConstructor.newInstance();
            //6, 判断两个对象是否相同
            System.out.println(hungry1 == hungry2);
        }
    }

    运行结果: false

    证明反射可以破坏单例对象唯一,新创建对象。

    如何防止反射对单例的攻击?

    既然反射攻击是调用默认构造器,那么反射在调用构造器时就抛出异常不让其创建对象。依然以饿汉式为例,修改默认构造方法,如果反射调用就抛出异常!

      private HungryTest() {
            if(null !=INSTANCE) {
                throw new RuntimeException("不支持反射调用默认构造器!");
            }
        };

    问:以上6种单例模式都可以通过在默认构造方法中抛异常防止暴力反射吗?

    答:除去枚举(其天然防止反射),其他5种分为2类,类初始化就创建对象为预加载方式,另一类为延迟加载方式;饿汉式、静态内部类为预加载方式 ,懒汉式、双检锁、双检锁变式为延迟加载方式。这里预加载可以用以上方法防止暴力反射,延迟加载不行,因为在默认构造方法中首先会对单例对象判空,延迟加载在获取单例时是没有创建对象的,这时可以通过反射创建对象,因此无法防止反射攻击,因此推荐的是枚举方式实现单例,省心省力。

    参考博客单例模式的攻击之反射攻击

    总结

    本文从单例模式的几种方式入手,分析每个的特点及问题,其中它们公共的特点是私有构造方法,再提供一个公开静态的方法供外部获取对象;我们在理解这几种方式原理后,能够很容易写出这些单例,分析每种方式存在的问题,以及改进的方式,其中线程安全问题,反序列化问题,反射问题应着重注意,如此我们也能较为全面了解单例模式。

  • 相关阅读:
    不常用的cmd命令
    js获取宽度
    Marshaling Data with Platform Invoke 概览
    Calling a DLL Function 之三 How to: Implement Callback Functions
    Marshaling Data with Platform Invoke 之四 Marshaling Arrays of Types
    Marshaling Data with Platform Invoke 之一 Platform Invoke Data Types
    Marshaling Data with Platform Invoke 之三 Marshaling Classes, Structures, and Unions(用时查阅)
    Calling a DLL Function 之二 Callback Functions
    WCF 引论
    Marshaling Data with Platform Invoke 之二 Marshaling Strings (用时查阅)
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/12423278.html
Copyright © 2011-2022 走看看