1。前言大家好,我是呈祥墨影。相信大家对单例模式应该不陌生。随便抓个程序员,让他说说最常用的三种设计模式,其中肯定包括单例模式。单例最重要的是要注意唯一性和线程安全问题。在Java中,单例的实现范式有很多,比如:饿汉式、懒汉式、静态内部类、双重检测等等,甚至可以利用枚举的特性来实现单例。上花样。其中,Hungry式单例实现代码最为简单。关键代码只需要一行staticfinal来声明对象。代码简单,满足要求。然而,饥饿的中国风却常常被我们“嫌弃”。每天review代码的时候,甚至看到一个HungryChinese单例的时候,都会“友好建议”对方使用doubledetection。饿了么风格依赖于JVM加载类的时机来完成静态对象的初始化。这个过程本身是线程安全的。最让人诟病的是它不能延迟加载,完全依赖于JVM加载类的时机,导致单例类加载的时机不可控。也有可能是有些资源还没有被业务使用,准备了单例类,导致系统资源占用过多。让我们再次回到Kotlin。在Kotlin中,实现单例就像将关键字class替换为object一样简单。objectSomeSingleton{funsayHi(){}}但是Kotlin的对象其实是中国式的单例。不怕资源占用的问题吗?2.Kotlin的对象2.1Kotlin的对象原理在开始为Kotlin的对象选择饿汉单例之前,我们先来了解一下Kotlin的对象原理。Kotlin和Java可以相互调用,Kotlin代码在运行前会被编译器编译成Java字节码。然后我们就可以使用工具将其还原为Java代码进行分析。AS原生支持此转换工具。借助AS的Tools→Kotlin→ShowKotlinBytecode,可以查看Kotlin编译出的Java字节码,然后点击Decompile按钮将字节码转换为Java代码。可以看出INSTANCE使用了staticfinal声明,并在static代码块中进行了初始化,是标准的饿汉式单例。2.2如何保证饿了么的唯一性和线程安全?前面说过,单例最重要的是要注意它的唯一性和线程问题。不管怎样,都要保证一个类的实例只有一个,不会因为多线程访问而创建多个实例。同时也不会因为多线程引入新的效率问题。Hungry式单例的原理其实是基于JVM的类加载机制来保证符合单例的规范。简单的说,JVM在加载一个类的时候,会经历初始化阶段(即Class被加载之后,被线程使用之前)。在初始化的时候,JVM会获取一把锁,可以同步多个线程,初始化一个类,保证只有一个线程完成类的加载过程。这一步是线程安全的。上图清楚的描述了类初始化锁的工作流程,这里不再赘述。3、所谓的Hungry-style问题上文提到,Hungry-style单例最受诟病的问题是无法实现懒加载,完全依赖于虚拟机加载类的策略加载。3.1懒加载懒加载的目的,说白了就是为了避免不必要的资源浪费,不需要的时候不加载,业务真正需要用到资源的时候才加载。虽然饿了么依赖虚拟机加载类的策略,但虚拟机本身也会有优化项,即“按需加载”策略。虚拟机在运行程序的时候,时不时的启动时并没有加载和初始化所有的类。而是采用了“按需加载”的策略,只有在实际使用的时候才会进行初始化。例如显式newClass()、调用类的静态方法、反射、Class.forName()等,当这些事件第一次发生时,会触发虚拟机加载类。例如,在上一篇文章中,让我们在应用程序中运行单例类SomeSingleton。应用程序首先启动,单击按钮时执行SomeSingleton.sayHi()方法。15:39:34.539I/cxmyDev:Apprunning15:39:44.606I/cxmyDev:SomeSingletoninit15:39:44.606I/cxmyDev:SomeSingletonsayHi注意Log的时间,只有点击按钮时才会执行SomeSingleton.sayHi(),单例类是虚拟机加载的。也就是说,通常只有当你真正使用这个类的时候,它才会真正被虚拟机初始化,我们不用担心被提前加载造成资源浪费。当然,不同的虚拟机有不同的实现,这不是强制的,但为了性能,大部分都会遵守这个规则。3.2从软件设计的角度看由于Hungry式的单例在第一次使用的时候也是初始化的,这自然是一种懒加载的效果。那我们换个角度想想。如果在程序启动的时候初始化了中国式单例,是不是有问题?在Java中,构造一个普通对象的成本其实很低。那为什么你在单例模式下会觉得是个问题呢?主要原因是单例生命周期长,承载业务和状态。如果不提前构造,无非就是两个问题。单例对象本身初始化复杂或耗时,过早初始化会影响其他业务;单例初始化后,占用过多资源,造成内存资源浪费;问题一:初始化逻辑复杂如果单例在初始化阶段,逻辑比较多,需要用到的时候再初始化,否则势必会影响接下来的业务性能。相反,它应该在此之前初始化,当系统相对空闲时。比如在Android下,空闲的时候可以使用IdleHandler提前做一些初始化工作。问题二:系统资源太多,资源永远不够用。缓存和性能必须随时保持平衡。单例作为一个生命周期长的类,不应该长期持有大量资源。否则,即使在加载时不报错,也难免埋下OOM隐患,这将是后面优化内存时关注的重点。在写代码的时候,想想内存资源的合理使用,而不是等到内存问题严重了,再着重于内存优化。合理利用弱引用来优化持有资源也是一种很好的优化方法。另外,如果在初始化时一定要占用一些资源,那么基于fail-fast原则,问题应该尽快暴露出来。毕竟,如果应用程序在开发者手中崩溃了,那才叫问题,如果在用户手中崩溃了,那才叫事故。4.总结时刻今天我们聊了聊Java单例和Kotlin对象单例的实现原理。最后,总结一下。Kotlin对象使用“hungry-style”单例,依靠JVM的类加载机制保证唯一性和线程安全;JVM使用“按需加载”策略加载类,保证延迟加载;Kotlin的对象选择饥饿式单例。实施实施都没有问题,使用起来也不用担心。
