单例模式-温故而知新

简介: 单例模式,应该是使用频率比较高的一种设计模式了。

前言


单例模式,应该是使用频率比较高的一种设计模式了。


关于它,你又了解多少呢?比如:


java和kotlin的实现方式?懒汉饿汉到底啥意思?


饿汉、双重校验、静态内部类模式的分别实现原理?


其中涉及到的类初始化、类锁、线程安全、kotlin语法知识?


静态变量实现单例——饿汉


保证一个实例很简单,只要每次返回同一个实例就可以,关键是如何保证实例化过程的线程安全


这里先回顾下类的初始化


在类实例化之前,JVM会执行类加载


而类加载的最后一步就是进行类的初始化,在这个阶段,会执行类构造器<clinit>方法,其主要工作就是初始化类中静态的变量,代码块。


<clinit>()方法是阻塞的,在多线程环境下,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>(),其他线程都会被阻塞。换句话说,<clinit>方法被赋予了线程安全的能力。


再结合我们要实现的单例,就很容易想到可以通过静态变量的形式创建这个单例,这个过程是线程安全的,所以我们得出了第一种单例实现方法:


private static Singleton singleton = new Singleton();
public static Singleton getSingleton() {
      return singleton;
}


很简单,就是通过静态变量实现唯一单例,并且是线程安全的。


看似比较完美的一个方法,也是有缺点的,就是有可能我还没有调用getSingleton方法的时候,就进行了类的加载,比如用到了反射或者类中其他的静态变量静态方法。所以这个方法的缺点就是有可能会造成资源浪费,在我没用到这个单例的时候就对单例进行了实例化。


在同一个类加载器下,一个类型只会被初始化一次,一共有六种能够触发类初始化的时机:


  • 1、虚拟机启动时,初始化包含 main 方法的主类;
  • 2、new等指令创建对象实例时
  • 3、访问静态方法或者静态字段的指令时
  • 4、子类的初始化过程如果发现其父类还没有进行过初始化
  • 5、使用反射API 进行反射调用时
  • 6、第一次调用java.lang.invoke.MethodHandle实例时


这种我不管你用不用,只要我这个类初始化了,我就要实例化这个单例,被类比为 饿汉方法。(是真饿了,先实例化出来放着吧,要吃的时候就可以直接吃了)


缺点就是 有可能造成资源浪费(到最后,饭也没吃上,饭就浪费了)


但其实这种模式一般也够用了,因为一般情况下用到这个实例的时候才会去用这个类,很少存在需要使用这个类但是不使用其单例的时候。


当然,话不能说绝了,也是有更好的办法来解决这种可能的资源浪费


在这之前,我们先看看Kotlin的 饿汉实现


kotlin 饿汉 —— 最简单单例


object Singleton


没了?嗯,没了。


这里涉及到一个kotlin中才有的关键字:object(对象)


关于object主要有三种用法:


  • 对象表达式


主要用于创建一个继承自某个(或某些)类型的匿名类的对象。


window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*……*/ }
    override fun mouseEntered(e: MouseEvent) { /*……*/ }
})


  • 对象声明


主要用于单例。也就是我们今天用到的用法。


object Singleton


我们可以通过Android Studio 的 Show Kotlin Bytecode 功能,看到反编译后的java代码:


public final class Singleton {
   public static final Singleton INSTANCE;
   private Singleton() {
   }
   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}


很显然,跟我们上一节写的饿汉差不多,都是在类的初始化阶段就会实例化出来单例,只不过一个是通过静态代码块,一个是通过静态变量。


  • 伴生对象


类内部的对象声明可以用 companion 关键字标记,有点像静态变量,但是并不是真的静态变量。


class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}
//使用
MyClass.create()


反编译成Java代码:


public final class MyClass {
   public static final MyClass.Factory Factory = new MyClass.Factory((DefaultConstructorMarker)null);
   public static final class Factory {
      @NotNull
      public final MyClass create() {
         return new MyClass();
      }
      private Factory() {
      }
      // $FF: synthetic method
      public Factory(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}


其原理还是一个静态内部类,最终调用的还是这个静态内部类的方法,只不过省略了静态内部类的名称。


要想实现真正的静态成员需要 @JvmField 修饰变量。


优化饿汉,吃饭的时候再去做饭 —— 最优雅单例


说回正题,即然饿汉有缺点,我们就想办法去解决,有什么办法可以不浪费这个实例呢?也就是达到 按需加载 单例?


这就要涉及到另外一个知识点了,静态内部类的加载时机。


刚才说到类的加载时候,初始化过程只会加载静态变量和代码块,所以是不会加载静态内部类的。


静态内部类是延时加载的,意思就是说只有在明确用到内部类时才加载。只使用外部类时不加载。


根据这个信息,我们就可以优化刚才的 饿汉模式,改成静态内部类模式(java和kotlin版本)


private static class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }
    public static Singleton getSingleton() {
        return SingletonHolder.INSTANCE;
    }


companion object {
        val instance = SingletonHolder.holder
    }
    private object SingletonHolder {
        val holder = SingletonDemo()
    }


同样是通过类的初始化<clinit>()方法保证线程安全,并且在此之上,将单例的实例化过程向后移,移到静态内部类。所以就变成了当调用getSingleton方法的时候才会去初始化这个静态内部类,也就是才会实例化静态单例。


如此一整,这种方法就完美了...吗?好像也有缺点啊,比如我调用getSingleton方法创建实例的时候想传入参数怎么办呢?


可以,但是需要一开始就设置好参数值,无法通过调用getSingleton方法来动态设置参数。比如这样写:


private static class SingletonHolder {
        private static String test="123";
        private static Singleton INSTANCE = new Singleton(test);
    }
    public static Singleton getSingleton() {
        SingletonHolder.test="12345";
        return SingletonHolder.INSTANCE;
    }


最终实例化进去的test只会是123,而不是12345。因为只要你开始用到SingletonHolder内部类,单例INSTANCE就会最开始完成了实例化,即使你赋值了test,也是单例实例化之后的事了。


这个就是 静态内部类方法的缺点了。如果不用动态传参数,那么这个方法已经足够了。


可以传参的单例 —— 懒汉


如果需要传参数呢?


那就正常写呗,也就是调用getSingleton方法的时候,去判断这个单例是否已存在,不存在就实例化即可。


private static Singleton singleton;
    public static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }


这个倒是看的很清楚,需要的时候才去创建实例,这样的话就保证了在需要吃饭的时候才去做饭,比较中规中矩的一个做法,但是在饿汉的思维里就会觉得这个人好懒啊,都不先准备好饭。


所以这个方法被称为 懒汉式


但是这个方法的弊端也是很明显,就是线程不安全,不同线程同时访问getSingleton方法有可能导致对象实例化出错。


所以,加锁。


双重校验的懒汉


加锁怎么加,也是个问题。


首先肯定的是,我们加的锁肯定是类锁,因为要针对这个类进行加锁,保证同一时间只有一个线程进行单例的实例化操作。


那么类锁就有两种加法了,修饰静态方法和修饰类对象:


//方法1,修饰静态方法
    public synchronized static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
//方法2,代码块修饰类对象
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }


方法2这种方式就是我们常说的双重校验的模式。


比较下两种方式其实区别也就是在这个双重校验,首先判断单例是否为空,如果为空再进入加锁阶段,正常走单例的实例化代码。


那么,为什么要这么做呢?


  • 第一个判断,是为了性能。当这个singleton已经实例化之后,我们再取值其实是不需要再进入加锁阶段的,所以第一个判断就是为了减少加锁。把加锁只控制在第一次实例化这个过程中,后续就可以直接获取单例即可。
  • 第二个判断,是防止重复创建对象。当两个线程同时走到synchronized这里,线程A获得锁,进入创建对象。创建完对象后释放锁,然后线程B获得锁,如果这时候没有判断单例是否为空,那么就会再次创建对象,重复了这个操作。


到这里,看似问题都解决了。


等等,new Singleton()这个实例化过程真的没问题吗?


在JVM中,有一种操作叫做指令重排


JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,会将指令进行重新排序,但是这种重新排序不会对单线程程序产生影响。


简单的说,就是在不影响最终结果的情况下,一些指令顺序可能会被打乱。


再看看在对象实例化中的指令主要有这三步操作:


  • 1、分配对象内存空间
  • 2、初始化对象
  • 3、instance指向刚分配的内存地址


如果我们将第二步和第三步重排一下,结果也是不影响的:


  • 1、分配对象内存空间
  • 2、instance指向刚分配的内存地址
  • 3、初始化对象


这种情况下,就有问题了:


当线程A进入实例化阶段,也就是new Singleton(),刚完成第二步分配好内存地址。这时候线程B调用了getSingleton()方法,走到第一个判空,发现不为空,返回单例,结果用的时候就有问题了,对象都没有初始化完成。


这就是指令重排有可能导致的问题。


所以,我们需要禁止指令重排,volatile 登场。


volatile 主要有两个特性:


  • 可见性。也就是写操作会对其他线程可见。
  • 禁止指令重排。


所以再加上volatile 对变量进行修饰,这个双重校验的单例模式也就完整了。


private volatile static Singleton singleton;


kotlin 版本双重校验


//不带参数
class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        Singleton() }
    }
}
//带参数
class Singleton private constructor(private val context: Context) {
    companion object {
        @Volatile private var instance: Singleton? = null
        fun getInstance(context: Context) =
                instance ?: synchronized(this) {
                    instance ?: Singleton(context).apply { 
                     instance = this 
                    }
                }
    }
}


诶?不带参数的这个写法也太简便了点吧?Volatile也没有了?确定没问题?


没问题,奥秘就在这个延迟属性lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED)中,我们进去瞧瞧:


public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this
    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }
            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }


看到了吧,其实内部还是用到了Volatile + synchronized 双重校验。


总结


今天和大家回顾了下单例模式,希望大家能有温故而知新的收获。


参考


https://www.kotlincn.net/docs/reference/object-declarations.html

目录
相关文章
|
19天前
|
设计模式 安全 Java
小谈设计模式(22)—单例模式
小谈设计模式(22)—单例模式
|
8月前
|
设计模式 缓存 Java
23种设计模式漫画版系列—单例模式
23种设计模式漫画版系列—单例模式
64 0
|
10月前
|
设计模式 C#
C# 机房重构单例模式
C# 机房重构单例模式
44 0
|
11月前
|
设计模式 安全 调度
设计模式——单例模式(面试手撕顶呱呱)
设计模式——单例模式(面试手撕顶呱呱)
|
设计模式 SQL 安全
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
297 0
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
|
设计模式 安全 Java
设计模式轻松学【二】你想学的单例模式都在这
一个类仅有一个实例,由自己创建并对外提供一个实例获取的入口,外部类可以通过这个入口直接获取该实例对象。
115 0
设计模式轻松学【二】你想学的单例模式都在这
|
安全 Java 编译器
单例模式 的那点儿事儿
单例模式 的那点儿事儿
86 0
|
设计模式 安全 Java
透彻理解单例模式
主要内容有: 该模式的介绍,包括: 引子、意图(大白话解释) 类图、时序图(理论规范) 该模式的代码示例:熟悉该模式的代码长什么样子 该模式的优缺点:模式不是万金油,不可以滥用模式 该模式的实际使用案例:了解它在哪些重要的源码中被使用
189 0
机房重构之单例模式的应用
机房重构之单例模式的应用
|
设计模式 前端开发
前端仔学学设计模式--单例模式
设计模式知识提取将分为N篇文章,本篇文章是个开篇文,后期会进行其他相关的同步(会就分享,不会就折腾),旨在提升技能,更好地享受敲键盘的快感~
前端仔学学设计模式--单例模式