规划形式篇之一文搞懂怎么完成单例形式

大家好,我是小简,这一篇文章,6种单例办法一网打尽,尽管单例形式很简略,可是也是规划形式入门基础,我也来详细讲讲。

设计模式篇之一文搞懂如何实现单例模式

DEMO仓库:github.com/JanYork/Des… ,欢迎PR,共建。

单例形式

单例形式(SingletonPattern)是 Java 中最简略的规划形式之一。

单例形式一共存在 –> 懒汉式、饿汉式、懒汉+同步锁、两层校验锁、静态内部类、枚举这六种办法。

这种类型的规划形式归于创立型形式,它供给了一种创立目标的最佳办法。

这种形式涉及到一个单一的类,该类负责创立自己的目标,一起确保只要单个目标被创立。

这个类供给了一种拜访其仅有的目标的办法,能够直接拜访,不需求实例化该类的目标。

要求

  • 单例类只能有一个实例。
  • 单例类有必要自己创立自己的仅有实例。
  • 单例类有必要给一切其他目标供给这一实例。

为什么需求运用单例形式

  1. 只允许创立一个目标,因而节省内存,加快目标拜访速度,因而目标需求被共用的场合适合运用,如多个模块运用同一个数据源衔接目标等等。
  2. 处理一个大局运用的类频频地创立与销毁问题。
  3. 其他场景自行脑部,单例即大局仅有目标,比方咱们所了解的SpringBean默认便是单例的,大局仅有。

单例原理

单例的原理十分简略,咱们让他仅有的办法便是让他不行用被new,那咱们只需求私有化类的结构即可:

private ClassName() {
}

可是私有化后,咱们不能new又怎么创立目标呢?

咱们首先要明白,private他是私有的,也便是不让外部其他类拜访,那咱们自己仍是能够拜访的,所以在上文的要求中就说到了:单例类有必要自己创立自己的仅有实例。

一起咱们还需求抛出单例的获取办法。

单例形式之懒汉式

创立单例类

public class SlackerStyle {
}

创立一个特点保存自身目标

public class SlackerStyle {
    private static SlackerStyle instance;
}

私有化结构

public class SlackerStyle {
    private static SlackerStyle instance;
    /**
     * 私有化结构办法(避免外部new新的目标)
     */
    private SlackerStyle() {
    }
}

自身创立目标与获取目标办法

public class SlackerStyle {
    private static SlackerStyle instance;
    /**
     * 私有化结构办法(避免外部new新的目标)
     */
    private SlackerStyle() {
    }
    /**
     * 供给一个静态的公有办法,当运用到该办法时,才去创立instance
     * 即懒汉式
     *
     * @return instance(单例目标)
     */
    public static SlackerStyle getInstance() {
        if (instance == null) {
            instance = new SlackerStyle();
        }
        return instance;
    }
}

当咱们调用静态办法,它便会判别上面的静态特点instance中有无自身目标,无 –> 创立目标并赋值给instance,有 –> 回来instance

优缺分析

长处:推迟加载,功率较高。

缺陷:线程不安全,或许会形成多个实例。

解说:推迟加载 –> 懒汉式只要在需求时才会创立单例目标,能够节约资源并进步程序的发动速度。

单例形式之懒汉式+锁

在以上的类中,对getInstance()办法增加synchronized锁,即可弥补线程不安全缺陷。

    /**
     * 留意,此段为弥补,为了处理线程不安全的问题,能够在办法上加上synchronized要害字,可是这样会导致功率下降
     * 供给一个静态的公有办法,加入同步处理的代码,处理线程安全问题
     * 此办法为线程安全的懒汉式,即懒汉+同步锁,就不额定写一个类了
     *
     * @return instance(单例目标)
     */
    public static synchronized SlackerStyle getInstance2() {
        if (instance == null) {
            instance = new SlackerStyle();
        }
        return instance;
    }

尽管弥补了线程不安全的缺陷,可是也失去了一部分功率,所以需求依据事务环境去挑选适合的办法,鱼和熊掌不行兼得。

单利形式之饿汉式

仍是如开端一样,创立好单例类,私有化结构办法。

public class HungryManStyle {
    /**
     * 私有化结构办法(避免外部new新的目标)
     */
    private HungryManStyle() {
    }
}

静态初始化目标

咱们饿汉式是推迟加载的,即要用,然后第一次去调用时才会创立目标,而饿汉式恰恰相反,他在初始化类的时分就去创立。

静态初始化?

咱们的static要害词润饰的办法或特点,在类加载之初遍拓荒内存创立好了相关的内容了。

包括每个类的:

static{
}

中也一样的。

所以咱们直接运用static润饰。

public class HungryManStyle {
    /**
     * 静态变量(单例目标),类加载时就初始化目标(不存在线程安全问题)
     */
    private static final HungryManStyle INSTANCE = new HungryManStyle();
    /**
     * 私有化结构办法(避免外部new新的目标)
     */
    private HungryManStyle() {
    }
    /**
     * 供给一个静态的公有办法,直接回来INSTANCE
     *
     * @return instance(单例目标)
     */
    public static HungryManStyle getInstance() {
        return INSTANCE;
    }
}

并且咱们在类的静态特点创立时就new了一个自身目标了。

优缺分析

饿汉式的长处如下:

  1. 线程安全:由于在类加载时就创立单例目标,因而不存在多线程环境下的同步问题。
  2. 没有加锁的功用问题:饿汉式没有运用同步锁,因而不存在加锁带来的功用问题。
  3. 完成简略:饿汉式的完成比较简略,不需求考虑多线程环境下的同步问题。

饿汉式的缺陷如下:

  1. 当即加载:由于在类加载时就创立单例目标,因而或许会影响程序的发动速度。
  2. 糟蹋资源:假如单例目标很大,并且程序中很少运用,那么饿汉式或许会糟蹋资源。

综上所述,饿汉式的长处是线程安全、没有加锁的功用问题和完成简略,缺陷是或许影响程序的发动速度和糟蹋资源。

在挑选单例形式的完成办法时,需求依据实践情况综合考虑各种要素,挑选最适合的办法。

单例形式之两层查看锁

初始化基本单例类

老规矩。

public class DoubleLockStyle {
    /**
     * volatile要害字,使得instance变量在多个线程间可见,禁止指令重排序优化
     * volatile是一个轻量级的同步机制,即轻量锁
     */
    private static volatile DoubleLockStyle instance;
    /**
     * 私有化结构办法(避免外部new新的目标)
     */
    private DoubleLockStyle() {
    }
}

不一样的是,我在特点上运用volatile要害词润饰了。

volatile?

弥补常识啦!

在这个代码中,运用了 volatile 要害字来确保 instance 变量的可见性,避免呈现空指针反常等问题。

  1. volatile是一种润饰符,用于润饰变量。
  2. 当一个变量被声明为volatile时,线程在拜访该变量时会强制从主内存中读取变量的值,而不是从线程的本地缓存中读取。
  3. 运用volatile要害字能够确保多线程之间的变量拜访具有可见性和有序性。
  4. 在对该变量进行修改时,线程也会将修改后的值强制刷回主内存,而不是只是更新线程的本地缓存。

弥补:

volatile的主要作用是确保同享变量的可见性和有序性。同享变量是指在多个线程之间同享的变量,例如单例形式中的 instance 变量。假如不运用volatile要害字润饰 instance 变量,在多线程环境下或许会呈现空指针反常等问题。

这是由于当一个线程修改了 instance 变量的值时,其他线程或许无法当即看到修改后的值,然后呈现空指针反常等问题。

运用 volatile 要害字能够处理这个问题,由于它能够确保对同享变量的修改对其他线程是可见的。

除了可见性和有序性之外,volatile 还能够避免指令重排序。指令重排序是指 CPU 为了进步程序履行的功率而对指令履行的顺序进行调整的行为。在单例形式中,假如 instance 变量没有被声明为 volatile,那么在多线程环境下或许会呈现单例目标被重复创立的问题。这是由于在多线程环境下,某些线程或许会在 instance 变量被初始化之前就调用 getInstance() 办法,然后导致多次创立单例目标。经过将 instance 变量声明为 volatile,能够确保在创立单例目标之前,instance 变量现已被正确地初始化了。

两层锁

/**
 * 供给一个静态的公有办法,加入两层查看代码,处理线程安全问题,一起处理懒加载问题
 * 即两层查看锁形式
 *
 * @return instance(单例目标)
 */
public static DoubleLockStyle getInstance() {
    if (instance == null) {
        // 同步代码块,线程安全的创立实例
        synchronized (DoubleLockStyle.class) {
            //之所以要再次判别,是由于或许有多个线程一起进入了第一个if判别
            if (instance == null) {
                instance = new DoubleLockStyle();
            }
        }
    }
    return instance;
}

在获取办法中,运用synchronized来同步,使它线程安全。

有缺分析

两层锁形式是一种用于推迟初始化的优化形式,在第一次调用时创立单例目标,并在之后的拜访中直接回来该目标。它经过运用两层查看锁定(double checked locking)来确保在多线程环境下只要一个线程能够创立单例目标,并且不会加锁影响程序功用。

长处:

  1. 线程安全:运用两层锁形式能够确保在多线程环境下只要一个线程能够创立单例目标,并且不会加锁影响程序功用。
  2. 推迟初始化:在第一次调用时创立单例目标,能够避免不必要的资源糟蹋和内存占用。
  3. 功用优化:经过运用两层查看锁定,能够避免不必要的锁竞争,然后进步程序功用。

缺陷:

  1. 完成杂乱:两层锁形式的完成相对杂乱,需求考虑线程安全和功用等要素,简略呈现错误。
  2. 可读性差:由于两层锁形式的完成比较杂乱,代码可读性较差,不易于了解和维护。
  3. 难以调试:由于两层锁形式涉及到多线程并发拜访,因而在调试过程中或许会呈现一些难以定位和复现的问题。

一个synchronized为何叫两层锁?

在两层锁形式中,确实只要一个 synchronized 要害字,可是这个 synchronized 要害字是在代码中被运用了两次,因而被称为“两层锁”。

详细来说,两层锁形式一般会在 getInstance 办法中运用 synchronized 要害字来确保线程安全,可是这会影响程序的功用,由于每次拜访 getInstance 办法都需求获取锁。为了避免这个问题,两层锁形式运用了一个优化技巧,即只要在第一次调用 getInstance 办法时才会获取锁并创立单例目标,以后的调用都直接回来现已创立好的单例目标,不需求再获取锁。

详细完成时,两层锁形式会在第一次调用 getInstance 办法时进行两次查看,别离运用外部的 if 句子和内部的 synchronized 要害字。外部的 if 句子用于判别单例目标是否现已被创立,假如现已被创立则直接回来单例目标,否则进入内部的 synchronized 要害字块,再次查看单例目标是否现已被创立,假如没有被创立则创立单例目标并回来,否则直接回来现已创立好的单例目标。

这样做的优点是,在多线程环境下,只要一个线程能够进入内部的 synchronized 要害字块,然后确保了线程安全,一起避免了每次拜访 getInstance 办法都需求获取锁的功用问题。

单例形式之静态内部类

由于现已了解了这个规划形式原理,我就直接放代码了。

public class StaticInnerClassStyle {
    /**
     * 私有化结构办法(避免外部new新的目标)
     */
    private StaticInnerClassStyle() {
    }
    /**
     * 静态内部类
     */
    private static class SingletonInstance {
        // 静态内部类中的静态变量(单例目标)
        private static final StaticInnerClassStyle INSTANCE = new StaticInnerClassStyle();
    }
    /**
     * 供给一个静态的公有办法,直接回来SingletonInstance.INSTANCE
     *
     * @return instance(单例目标)
     */
    public static StaticInnerClassStyle getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

优缺分析

长处:

  1. 线程安全:静态内部类在第一次运用时才会被加载,因而在多线程环境下也能够确保只要一个线程创立单例目标,避免了线程安全问题。
  2. 推迟加载:静态内部类形式能够完成推迟加载,即只要在第一次调用 getInstance 办法时才会加载内部类并创立单例目标,避免了在程序发动时就创立单例目标的开支。

缺陷:

  1. 需求额定的类:静态内部类形式需求界说一个额定的类来完成单例形式,假如项目中有大量的单例目标,则会增加代码量。
  2. 无法传递参数:静态内部类形式无法接受参数,因而无法在创立单例目标时传递参数,这或许会对某些场景形成约束。

总的来说,静态内部类形式是一种功用高、线程安全的单例形式完成办法,适用于大部分场景。

假如需求传递参数或者需求频频创立单例目标,则或许需求考虑其他的完成办法。

它不是static润饰吗?为什么也能够懒加载

懒加载即延时加载 –> 运用时采纳创立目标。

在静态内部类形式中,单例目标是在静态内部类中被创立的。静态内部类只要在第一次被运用时才会被加载,因而单例目标也是在第一次运用时被创立的。这样就完成了推迟加载的效果,即在需求时才创立单例目标,避免了在程序发动时就创立单例目标的开支。

此外,静态内部类中的静态变量和静态办法是在类加载时被初始化的,而静态内部类自身是十分轻量级的,加载和初始化的时间和开支都十分小。因而,静态内部类形式既能够完成懒加载,又不会带来太大的功用损失。

总归,它在静态初始化意料之外,我信任也在你意料之外。

单例形式之枚举单例

/**
 * @author JanYork
 * @date 2023/3/1 17:54
 * @description 规划形式之单例形式(枚举单例)
 * 长处:避免序列化和反序列化进犯损坏单例,避免反射进犯损坏单例(枚举类型结构函数是私有的),线程安全,推迟加载,功率较高。
 * 缺陷:代码杂乱度较高。
 */
public enum EnumerateSingletons {
    /**
     * 枚举单例
     */
    INSTANCE;
    public void whateverMethod() {
        // TODO:do something ,在这里完成单例目标的功用
    }
}

在上述代码中,INSTANCEEnumSingleton 类型的一个枚举常量,表示单例目标的一个实例。由于枚举类型的特性,INSTANCE 会被自动初始化为单例目标的一个实例,并且确保在整个应用程序的生命周期中只要一个实例。

运用枚举单例的办法十分简略,只需求经过 EnumSingleton.INSTANCE 的办法来获取单例目标即可。例如:

EnumerateSingletons singleton = EnumerateSingletons.INSTANCE;
singleton.doSomething();

运用枚举单例的优点在于,它是线程安全、序列化安全、反射安全的,并且代码简洁明了,不简略犯错。别的,枚举单例还能够经过枚举类型的特性来增加其他办法和特点,十分灵活。

优缺分析

  1. 线程安全:枚举类型的实例创立是在类加载的时分完成的,因而不会呈现多个线程一起拜访创立单例实例的问题,确保了线程安全。
  2. 序列化安全:枚举类型默认完成了序列化,因而能够确保序列化和反序列化过程中单例的一致性。
  3. 反射安全:由于枚举类型的特殊性,不会被反射机制创立多个实例,因而能够确保反射安全。
  4. 简洁明了:枚举单例的代码十分简洁,易于了解和维护。

枚举单例的缺陷相对来说比较少,可是也存在一些约束:

  1. 不支持懒加载:枚举类型的实例创立是在类加载的时分完成的,因而无法完成懒加载的效果。
  2. 无法承继:枚举类型不能被承继,因而无法经过承继来扩展单例类的功用。
  3. 有些情况下不太便利运用:例如需求传递参数来创立单例目标的场景,运用枚举单例或许不太便利。

总归,枚举单例是一种十分优异的单例完成办法,它具有线程安全、序列化安全、反射安全等长处,适用于大多数单例场景,但也存在一些约束和局限性。需求依据详细的场景来挑选合适的单例完成办法。

这么多办法我该怎么选?

规划形式本便是事务中优化一些规划带来的概念性规划,咱们需求结合事务分析:

  1. 饿汉式:适用于单例目标较小、创立本钱低、不需求懒加载的场景。
  2. 懒汉式:
    • 两层锁:适用于多线程环境,对功用要求较高的场景。
    • 静态内部类:适用于多线程环境,对功用要求较高的场景。
  3. 枚举:适用于单例目标创立本钱较高,且需求考虑线程安全、序列化安全、反射安全等问题的场景。

假如你的单例目标创立本钱低、不需求考虑线程安全、序列化安全、反射安全等问题,主张运用饿汉式完成单例;假如需求考虑线程安全和功用问题,能够挑选懒汉式的两层锁或静态内部类完成办法;假如需求考虑单例目标创立本钱较高,需求考虑线程安全、序列化安全、反射安全等问题,主张挑选枚举单例完成办法。

当然,在实践的开发中,还需求考虑其他一些要素,如单例目标的生命周期、多线程拜访情况、功用要求、并发拜访压力等等,才能综合挑选最合适的单例完成办法。

Java程序员身边的单例形式

来自某AI(敏感词):

设计模式篇之一文搞懂如何实现单例模式