• 设计模式の单列模式


    设计模式の单列模式

    所谓单列模式

    单列模式是指确保一个类在任何情况下都绝对只有一个实例,并对外提供一个全局的访问点

    比如:ServletContext、SeevletContextConfig、ApplicationContext、数据库连接池 ......

    但创建单列的方式有很多种,下面我们一一来学习

    饿汉式单列

    饿汉式适用在单例对象较少的情况,Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例

    懒汉式单列

    在上面的图中,我们说他的缺点,只能在单线程中使用,

    • 下面我们就用手动控制多线程运行进度的方式来破解这种单列

    道高一尺,魔高一丈,居然大家都知道这种破解方法了,那就只有进化了:synchronized

    • 要想使得懒汉式在多线程环境下保证自己的单列,那就只有让那个判断变为线程同步方法了

    我们再次开启线程调试模式进行debug

    • 我们发现当两个线程都被我们同步控制到 getInstance()方法时,一个线程显示为running,一个线程状态显示为monitor(阻塞)

    • 直到我们第一个县城执行完,返回的时候,第二个线程状态才变更为running,此时再判断singleton3 == null 明显就是false了,保证了单列

    • 但是如果我们的服务的线程并发情况比较严重,那么获得该实列的成本就高了起来,线程阻塞情况逐渐严重

    • 完美版本,兼顾多线程环境,性能得到保证:双重检查锁

    • 第一次判断,并发线程都可以进入,即使全部判断为true

    • 其中一个线程获得块级锁synchronized执行权,进行执行,进入其中,再次判断也为true,创建对象并返回

    • 其他同步并发线程依次进入块级锁synchronized,再次进行判断,结果为false,直接返回

    • 除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题

    • 这其中涉及到一个关键字:volatile

    为什么要加引用修饰符:volatile

    • singleton3 = new Singleton3(); 这行代码对于JVM而言可以依次分解为三步

      1. 分配内存空间

      2. 初始化对象

      3. 将对象引用指向刚分配的内存空间

    • 为了提高程序执行性能,编译器和处理器会对指令的处理过程进行:重排序机制

    • 上面依次执行三步,2和3就变换了位置,这或许就是真正的

      1. 分配内存空间

      2. 将对象引用指向刚分配的内存空间

      3. 初始化对象

    • 如果我们的对象引用 singleton3 没有加 volatile 关键字,这个单列可能就被破坏了

    • 试想

      • A、B两个线程同时进入第一次 if (singleton3 == null) ,均拿到为true的结果

      • A分配到了CPU执行权,进入块级锁,

        • 这时发生了重排序机制,初始化对象这一步变成了第三步 , 还没执行完,方法返回了,释放锁

        • B线程进入块级锁,再次判断,if (singleton3 == null) ,也得到结果为:true,再次创建对象返回

        • 单列模式被破坏,GG

    • 经过volatile修饰的变量,如果一个线程修改了该变量的值,会立刻刷新到主内存区域,其他线程要读该变量的值,必须要写完之后。

    • 也就是B线程在第二次判断if (singleton3 == null)时,必须等到线程A对该变量写完初始化成功后再读取

      • 于是 if (singleton3 == null) 就会得到结果为false,直接返回线程A创建的对象实列

    静态内部类单列

    反射破解单列

    就拿我们目前为止觉得很OK的静态内部类单列,我们来破解他的单列

    可以发现我们是通过强制访问 Singleton7 无参构造来实现的暴力初始化,为了防止他暴力访问无参方法创建实例,咱也来写一把牛逼的代码

    • 我们在私有的构造方法中加点颜色,防止他暴力访问

    序列化破解单列

    当我们将一个单例对象创建好,然后序列化为字符串,然后再将字符串反序列化为对象

    • 反序列化后的对象会重新分配内存, 即重新创建,单列又被破坏了哦

    序列化的方式分为很多种,上面我们使用三方库的方式将其序列化为字符串,然后再反序列化为对象,可见单列已经被破坏

    网上还有一种序列化方式是可以防止单列被破坏的:下面我们来大概看一下

    • 将创建好的单列对象通过IO流,刷盘到磁盘中,然后再通过IO流去读取

    • 至于原因是什么,大家可以去看看ObjectInputStream的readObject()方法的源码

      • JDK中,通过明文指定名为readResolve属性,反射得到该方法,如果该方法存在,则返回该方法返回的实列作为反序列化的引用

      • 如果该方法不存在,则会从新创建一个新的对象,并完成分配内存、初始化对象、将对象引用指向刚分配的内存空间的过程

    注册式单列(枚举)

    注册式单例有两种写法:一种为枚举登记,一种为容器缓存

    此外,当我们使用序列化的方式尝试破坏枚举的单列时,是不行的,通过反射的方式爱破坏也是不行的

    枚举的方式是一种推荐的单列模式的实现方式

    注册式单列(容器缓存)

    容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的

    public class ContainerSingleton {
        private ContainerSingleton(){}
        private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
        public static Object getBean(String className){
            synchronized (ioc) {
                if (!ioc.containsKey(className)) {
                    Object obj = null;
                    try {
                        obj = Class.forName(className).newInstance();
                        ioc.put(className, obj); 
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                    return obj;
                } else {
                    return ioc.get(className);
                }
            }
        }
    }

    ThreadLocal线程单列

    ThreadLocal 不能保证其 创建的对象是全局唯一,但是能保证在单个线程中是唯一的

    那么ThreadLocal又是如何保证线程隔离的呢

    ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的

    总结

    单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。

     

     

  • 相关阅读:
    Docker删除某个容器时失败解决方案
    Docker搭建redis
    Django优雅集成MongoDB
    MongoDB学习笔记:文档Crud Shell
    MongoDB学习笔记:MongoDB 数据库的命名、设计规范
    MongoDB学习笔记:快速入门
    MongoDB学习笔记:Python 操作MongoDB
    在Docker中安装MongoDB
    Linux 挂载盘
    java中Array/List/Map/Object与Json互相转换详解(转载)
  • 原文地址:https://www.cnblogs.com/msi-chen/p/15570249.html
Copyright © 2020-2023  润新知