单例模式

单例模式

捡破烂的诗人 731 2022-07-10
  • 联系方式:1761430646@qq.com
  • 菜狗摸索,有误勿喷,烦请联系

1. 单例模式

1.1 基础

  1. 概念

    单例模式是指在内存中创建并且只会创建一次对象的设计模式,在程序中多次使用同一个对象且作用相同时,为了防止频繁创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象

    • 应用场景:需要 频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级
      对象),但又经常用到的对象、 工具类对象、频繁访问数据库或文件的对象(比如 数据源、session 工厂等)

    1-1657434174339

  2. 单例模式的类型

    1. 懒汉式:在真正需要使用该对象的时候才会取创建
      • 在程序使用对象前,先判断该对象是否已经被实例化,如果已实例化则直接返回该对象,否则先执行实例操作
      • 2-1657434174354
      • 3-1657434174372

    1. 饿汉式:在类加载的时候就会创建该对象,等待被使用不能延时加载
    • 在类加载的时候就会创建好改对象,程序调用时直接返回该单例对象(即编码时指定马上创建这个对象)
    • 4-1657434176955
    • 5-1657434176950
    1. 使用静态内部类创建单例模式
    public class Singleton {
    	private static class SingletonHoler {
    		/**
    		 * 静态初始化器,由JVM来保证线程安全
    		 */
    		private static Singleton instance = new Singleton();
    	}
    
    	private Singleton() {
    	}
    
    	public static Singleton getInstance() {
    		return SingletonHoler.instance;
    	}
    
    
    • 当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

1.2 深入

  • 懒汉式如何保证只会创建一个对象

    3-1657434174372

    • 上面的懒汉式单例模式创建对象是存在问题的,比如当两个线程同时判断singleItem为空,那么他们就会同时实例化一个SingleItem对象,这样就变成多例了----所以,这里要解决的是线程安全问题

    • 解决方案:

      1. 给方法加锁或者给类对象加锁

        6-1657434176957

        7-1657434179495

    • 上述加锁操作已经规避了多个线程同时创建SingleItem对象的风险了,但是却引来另外一个问题

      • 每次取获取对象的时候都需要先获取锁,并发性能非常地差,极端情况下可能会出现卡顿现象
    • 接下来是性能优化目标如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁直接获取实例

      • 8

      • 个人理解:

        前提:当在00:00时刻时,有1000000个线程同时运行到这一步

        • 采用给方法或对象加锁

          这时候其中一个线程获得锁,其他线程等待,待这个线程解锁后,其他99个线程也依次重复等待,加锁,解锁的过程,当00:01时刻时,多个线程也同时运行到一步,那么也是重复上述过程

        • 采用性能优化的方法加锁

          这时候其中一个线程获得锁,其他线程等待,待这个线程解锁后,其他99个线程也依次重复等待,加锁,解锁的过程(之所以还需要判断是否为空,是因为第一个线程实例化对象后,后面的线程获得锁后进入分支会判断是否已经实例化,如果已经实例化则直接退出来返回该对象,否则实例化该对象),当00:01时刻时,多个线程也同时运行到这一步,因为发现之前已经实例化过对象了,所以直接返回该对象,不必加锁等待过程

          总结:性能优化的策略是在实例化对象的时候可以理解为会稍微‘顿’一下,只要实例化后,后面不管多少个线 程同时来,都会直接返回该对象,会飞起

          而采用给方法或对象加锁的策略,实例化的时候会可以理解为会稍微‘顿’一下,而后面的时刻多个线程 来时都会重复加锁过程,也会稍微’顿’一下

      • 因为需要两次判空,且对类对象加锁,该懒汉式写法也称为:Double Check(双重校验) + Lock(加锁)

    • 上述方案已经几乎很完善了,但还存在着指令重排问题使用volatile防止指令重排

      创建一个对象,在JVM中会经过三步

      1. 为singleItem分配内存空间
      2. 初始化singleItem对象
      3. 讲singleItem指向好分配好的内存空间
      • 指令重排是指:JVM在保证最终结果正确的情况下,可以不按照编码的顺序执行语句,尽可能提高程序的性能

      在这三步中,第2,3步有可能发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行力1,3步骤,线程B判断singleton已经不为空,获取到未初始化的singleItem对象,就会报NPE异常

      8

    • volatile:

      1. 使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变化
      2. volatile还有第二个作用,就是使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量
    • 懒汉式最终代码实现:

      9-1657434182084


  • 破坏

    • 上述无论是多么完美的懒汉式和饿汉式或者是使用静态内部类创建单例模式,最终都敌不过反射和序列化还有克隆,他们都可以把单例对象破坏掉(产生多个不同对象)

    • 利用反射破坏单例模式

      10-1657434182101

      利用反射,强行访问类的私有构造器,去创建另外一个对象

    • 利用序列化与反序列化破坏单例模式–单例类要实现Serializable接口

      11-1657434182117

      两个对象地址不相等是因为:一开始内存中有个first的对象在A的内存空间中,后面从文件中读取对象出来,会让		second指向B的内存空间中    
      

      12-1657434184670

    • 利用克隆clone破坏单例模式–单例类需要实现Cloneable接口,重写Clone方法

      • 单例类代码

        18-1657434189612

      • 测试类

        19-1657434189643


    • 防止破坏

      • 防止反射破坏单例模式
      • 可以定义一个布尔型全局变量默认为fasle。

      16-1657434187081

      23-1657434191864


      • 防止序列化破坏单例模式
      • 可以重写readSolve()方法

      17-1657434187063

      • 防止克隆破坏单例模式
      • 可以重写clone方法

      20-1657434189627

      22-1657434191879


1.3 终极无敌单例模式

  • 从JDK1.5后,可以使用枚举来构造单例模式

  • 实现代码:

    13-1657434184677

  • 显示优点:

    1. 一目了然的代码,代码量很少

    2. 线程安全和单一实例

      • 上述测试代码可以看出程序启动时仅仅会创建一个SingleItem对象,且是线程安全的
      • 可以简单理解为枚举创建实例的过程:在程序启动时,会调用SingleItem的空参构造器,实例化好了一个SingleItem对象赋给INSTANCE,之后再也不会实例化了
    3. 枚举保护单例模式不会破坏

      • 使用枚举可以防止调用者使用反射,序列化与反序列化机制强制生成多个单例对象,破坏单例模式
      • 14-1657434184673
      • 枚举类默认继承了Enum类,在利用反射调用newInstance()时,会判断该类是否是一个枚举类,如果是,则抛出异常
      • 在读入SingleItem对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,使用Enum类的valueOf(String name)方法根据变量的名字查找对应的枚举对象
      • 所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型以及名字,没有任何关于对象的操作
      • 15-1657434187046

# 设计模式 # 单例模式