设计模式の单列模式

设计模式の单列模式

所谓单列模式

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

比如: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 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的

总结

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

 

 

posted @ 2021-11-17 23:55  鞋破露脚尖儿  阅读(101)  评论(0编辑  收藏  举报