关于后端:设计模式篇之一文搞懂如何实现单例模式

100次阅读

共计 7281 个字符,预计需要花费 19 分钟才能阅读完成。

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

大家好,我是小简,这一篇文章,6 种单例办法一网打尽,尽管单例模式很简略,然而也是设计模式入门根底,我也来具体讲讲。

DEMO 仓库:https://github.com/JanYork/DesignPattern,欢送 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(敏感词):

正文完
 0