再谈单例模式

2024-09-18 07:08:01 浏览数 (1)

前言

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

  1. 静态内部类的实现方案,为何是线程安全的?
  2. DCL优化(双重校验模式),为何会线程不安全?又该如何优化?
  3. 枚举类为何天生特殊,一定线程安全?

概念

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

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

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

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

普通模式(非线程安全)

代码语言:txt复制

public class SingletonClassV1 {

    private static SingletonClassV1 INSTANCE = null;

    private SingletonClassV1() {}

    /**
     * 非线程安全
     * @return
     */
    public static SingletonClassV1 getInstance(){
        if (INSTANCE == null) {
            INSTANCE = new SingletonClassV1();
        }
        return INSTANCE;
    }

}
  • 不足:非线程安全,并发情况下,可能创建了多个实例

饿汉模式(线程安全)

代码语言:txt复制
public class SingletonClassV2 {

    //类初始化时,就已经创建对象,因此线程安全
    private static SingletonClassV2 INSTANCE = new SingletonClassV2();

    private SingletonClassV2() {}

    /**
     * 线程安全
     * @return
     */
    public static SingletonClassV2 getInstance(){
        if (INSTANCE == null) {
            INSTANCE = new SingletonClassV2();
        }
        return INSTANCE;
    }

}
  • 好处:类在加载时就直接初始化了实例。即使没用到,也会实例化,因此,它也是线程安全的单例模式。
  • 不足:导致系统加载时间变长,同时也提前占用资源(有没有按需使用资源的场景呢?)

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

代码语言:txt复制

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优化(双重校验模式)

代码语言:txt复制
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每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

静态内部类(线程安全)

代码语言:txt复制
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模式里自己斟酌。

枚举类(线程安全)

代码语言:txt复制
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实例,它们在类加载时被创建并初始化。

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

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

代码语言:txt复制
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();
    }
}

0 人点赞