重修设计模式-创建型-单例模式

重修设计模式-创建型-单例模式

一个类只允许创建一个对象(或实例),那这个类就是一个单例类,这种模式叫做单例设计模式。

单例的主要使用场景有两个,一是使用单例控制全局的资源访问,也就是用单例封装一些工具类,比如日志写入工具;二是有些数据在系统中应该只保存一份,比如应用程序的用户登录信息,需要用单例设计一个用户信息管理类。

单例的创建又有饿汉式、懒汉式、双重检测,甚至利用 JVM 虚拟机来创建单例对象,下面介绍一下这些创建方式。

一、饿汉式(着急吃)

在类加载时就将类的实例对象创建好了,是一种提前初始化的行为。

因为实例对象会占用内存或其他资源,所以这种提前初始有些人认为是一种资源浪费,应该在使用时再进行。但世事无绝对,如果你是后端开发人员,在接口请求到来时再去进行初始化操作,就会导致请求的响应时间变长甚至超时;如果你是前端开发,由于内存和资源的宝贵,则完全可以将单例初始化平摊到后续的用户操作中。

这种方式有优点也有缺点,具体使用还要根据使用场景,Kotlin 中的 Object 关键字就可以生命一个单例的类,实例默认就是饿汉式创建的:

object SingleClass {
    fun doSoming() {
        println("doSoming...")
    }
}

//使用:
SingleClass.doSoming()

二、懒汉式(不着急)

懒汉式指的是将实例对象的创建放到使用时,也就是延迟初始化。

class SingleInstance2 private constructor() {
    companion object {
        private lateinit var sInstance: SingleInstance2
        @Synchronized
        fun getInstance(): SingleInstance2 {
            if (!sInstance::isInitialized) { //判断是否已经初始化
                sInstance = SingleInstance2()
            }
            return sInstance
        }
    }

    fun foo() {
        println("do...")
    }
}

这里用 Java 举例,会将实例初始化过程使用 synchronized 锁住,但加锁是非常损耗性能的操作,所以非常不推荐这样去写,更常见的写法应该是下面的双重检测。

三、双重检测

其实也是懒汉式思想,只是优化了实现方式。双重检测的标准化代码如下:

//懒汉式-双重检测
class SingleInstance3 private constructor() {
    companion object {
        @Volatile   //防止指令重排
        private var sInstance: SingleInstance3? = null
        fun getInstance(): SingleInstance3 {
            if (sInstance == null) {    //第一次检测,为了避免加锁
                synchronized(this) {
                    if (sInstance == null) {    //第二次检测,为了防止并发情况重复初始化
                        sInstance = SingleInstance3()
                    }
                }
            }
            return sInstance!!
        }
    }

    fun foo() {
        println("do...")
    }
}

第一次检测是在实例已经创建出来后,避免加锁逻辑;

第二次检测是为了防止多线程锁竞争时,导致的重复初始化。比如线程A通过了第一次检测,此时切换到线程B,由于 sInstance 还没被赋值,线程B也通过了第一次检测,此时无论哪个线程进入加锁代码,另一个线程都会等待锁释放后再进入加锁代码,这时就会导致重复初始化。

volatile 关键字则是为了防止指令重排导致的没有完整进行初始化的对象被使用场景。这里就要介绍一下实例的创建流程,分为三步:

  1. 分配内存(JVM 堆,为实例变量分配空间和赋予默认值(零值))
  2. 初始化对象(包括实例变量初始化(赋予真正的值),示例代码块初始化,构造函数初始化)
  3. 将内存地址指向引用对象本身

正常顺序是123,如果指令重排序后是132,在执行到3时,另一个线程进入获取对象,这时虽然 sInstance 不为空,但还未执行初始化操作,就会导致获取到的对象是未初始化的对象。

双重检测的代码是非常模板化的,可以进一步封装这些代码(Kotlin语言):

/**
 *  description : Double Check 方式单例封装(由伴生对象继承)
 */
abstract class BaseSingleton<out T> {
  @Volatile
  private var instance: T? = null
  protected abstract fun creator(): T

  fun getInstance(): T = instance ?: synchronized(this) {
    instance ?: creator().also { instance = it }
  }
}

使用时用伴生对象去继承:

class SingleInstance6 private constructor() {
    companion object: BaseSingleton<SingleInstance6>() {
        override fun creator(): SingleInstance6 {
            return SingleInstance6()
        }
    }
}

四、利用 JVM 规范初始化单例对象

  1. 静态内部类

    public class SingleInstance4 {
      private SingleInstance4() {}
    
      private static class SingletonHolder{
        private static final SingleInstance4 instance = new SingleInstance4();
      }
      
      public static SingleInstance4 getInstance() {
        return SingletonHolder.instance;
      }
     
      public void foo() {
        System.out.println("do...");
      }
    }
    

    这种方式利用的就是 JVM 的类加载时机,SingletonHolder 是一个静态内部类,当外部类 SingleInstance4 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

    1. 枚举
    public enum SingleInstance5 {
      INSTANCE;
    
      public void foo() {
        System.out.println("do...");
      }
    }
    

    通过 Java 枚举类型本身的特性,可以保证实例创建的线程安全性和实例的唯一性。

总结

一个类只有一个实例对象就是单例模式。

实现单例需要注意的点:

  1. 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
  2. 考虑对象创建时的线程安全问题;(Double Check)
  3. 考虑是否支持延迟加载;(懒汉式)
  4. 考虑 getInstance() 性能是否高(是否加锁🔒,Double Check)
  • 20
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值