您的位置:首页 > 文旅 > 美景 > 再谈单例模式

再谈单例模式

2024/12/24 2:00:15 来源:https://blog.csdn.net/qq_29166327/article/details/141339310  浏览:    关键词:再谈单例模式

前言

此前写过设计模式的文章:《单例模式》,谈过单例模式,但对背后的底层知识阐述的还不够到位,比如下面几个问题剖析的不够仔细:

  1. 静态内部类的实现方案,为何是线程安全的?

  2. DCL优化(双重校验模式),为何会线程不安全?又该如何优化?

  3. 枚举类为何天生特殊,一定线程安全?

概念

创建型模式是用来创建对象的模式,抽象了实例化的过程,帮助一个系统独立于其他关联对象的创建、组合和表示方式。

单例模式的目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式也是创建型的设计模式之一,本文是设计模式系列(共24节)的第2篇文章。设计模式是基于六大设计原则进行的经验总结:《第一节:设计模式的六大原则》创建型设计模式共5种:
单例模式(Singleton Pattern):一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式(Singleton Pattern)可以说是整个设计中最简单的模式之一,且这种模式即使在没有看设计模式相关资料也经常在编码开发中。因为在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。

综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。

普通模式(非线程安全)

public class SingletonClassV1 {private static SingletonClassV1 INSTANCE = null;private SingletonClassV1() {}/*** 非线程安全* @return*/public static SingletonClassV1 getInstance(){if (INSTANCE == null) {INSTANCE = new SingletonClassV1();}return INSTANCE;}}
  • 不足:非线程安全,并发情况下,可能创建了多个实例


饿汉模式(线程安全)

public class SingletonClassV2 {//类初始化时,就已经创建对象,因此线程安全private static SingletonClassV2 INSTANCE = new SingletonClassV2();private SingletonClassV2() {}/*** 线程安全* @return*/public static SingletonClassV2 getInstance(){if (INSTANCE == null) {INSTANCE = new SingletonClassV2();}return INSTANCE;}}
  • 好处:类在加载时就直接初始化了实例。即使没用到,也会实例化,因此,它也是线程安全的单例模式。

  • 不足:导致系统加载时间变长,同时也提前占用资源(有没有按需使用资源的场景呢?)


懒汉模式(加锁&线程安全)

public class SingletonClassV3 {//类初始化时,就已经创建对象,因此线程安全private static SingletonClassV3 INSTANCE = null;private SingletonClassV3() {}/*** 线程安全* @return*/public static synchronized SingletonClassV3 getInstance(){if (INSTANCE == null) {INSTANCE = new SingletonClassV3();}return INSTANCE;}}
  • 好处:懒加载了,也线程安全了

  • 不足:将方法强行锁了,可能导致性能问题(有没有性能更好一点的办法呢?)


懒汉模式-DCL优化(双重校验模式)

public class SingletonClassV4 {// 加了volatile,就能解决【1】的问题private static volatile SingletonClassV4 INSTANCE = null;private SingletonClassV4() {}/*** 【1】JVM的指令重排序,可能导致并发下的重复创建* @return*/public static SingletonClassV4 getInstance(){// 第一次检测if (INSTANCE == null) {synchronized (SingletonClassV4.class) {// 第二次检测if (INSTANCE == null) {INSTANCE = new SingletonClassV4();}return INSTANCE;}}return INSTANCE;}}

好处:懒加载了,只锁一部分代码段

不足:可能因为JVM存在乱序执行功能,DCL也会出现线程不安全的情况

  • 不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,

  • 即在JDK1.6及以后,只要定义为 private volatile static SingleTon  INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。


静态内部类(线程安全)

package com.bryant.singleton;public class SingletonClassV5 {private static class SingleTonHoler{private static SingletonClassV5 INSTANCE = new SingletonClassV5();}/*** 私有化构造器*/private SingletonClassV5() {}/*** 获取单例方法,getInstance()获取单例的方法,不会触发多次new操作,所以只会返回同一个对象* @return*/public static SingletonClassV5 getInstance() {return SingleTonHoler.INSTANCE;}public static void main(String[] args) {SingletonClassV5 instance = SingletonClassV5.getInstance();System.out.println(instance.hashCode());}}

好处:用到了静态内部类的懒加载特性,做到了线程安全

静态内部类的特殊性

JVM主动加载类

JVM有5个主动引用而类加载的场景,分别是:

  1. 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。

  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。

  3. 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。

  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类

  5. 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。


静态内部类-被动加载

而静态内部类并不在5种情况之内,所以静态内部类,是绝对是用到了才会加载的资源,所以不会触发提前加载。

因此,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

静态内部类-线程安全

当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。可以参考:Java虚拟机:浅谈静态代码块和方法

不足

是不是可以说静态内部类单例就是最完美的单例模式了呢?

其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

枚举类(线程安全)

public class SingletonClassV6 {enum SingletonEnum {INSTANCE;//懒加载,创建一个枚举对象,该对象天生为单例private SingletonClassV6 singleton;//私有化枚举的构造函数(强调不可外部实例化)private SingletonEnum() {singleton = new SingletonClassV6();}public static SingletonClassV6 getEnumInstance(){return INSTANCE.singleton;}}public static SingletonClassV6 getInstance(){return SingletonEnum.getEnumInstance();}}
  • 好处:实现了懒加载

    • 枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。

  • Java编译器会将枚举类,转换为一个继承自java.lang.Enum的类。这意味着枚举本质上是一个特殊的类。

  • 枚举常量是该枚举类的静态final实例,它们在类加载时被创建并初始化。


模式应用:日志工具类(静态内部类)

企业应用按规范去打印日志,只要一个单例工具类完成即可。

public class BusinessLogUtil {private static Logger logger = LoggerFactory.getLogger(BusinessLogUtil.class);private BusinessLogUtil() {}public static final BusinessLogUtil getInstance() {return SingletonHolder.INSTANCE;}private static class SingletonHolder {private static final BusinessLogUtil INSTANCE = new BusinessLogUtil();}
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com