单例形式

文章的初衷

问个问题,饿汉式单例的缺陷是调用时或许会形成内存耗费。那么能讲下,它究竟是怎么耗费内存*…的呢?里边的原理**是什么呢?

假如你对这个问题存疑,那么我引荐你看这篇文章,并且或许单例真的不像你想的那么简略!

本文意图是对常识的一个总结,俗语说的好,好记性不如烂笔头。常常总结,也能帮助总结常识点增强回忆。另一个意图是期望能够科普单例所触及的常识点。

期望读了本文之后,再遇到面试官问你单例的相关常识点时,你能够胸中有数,让他对你刮目相看

几个小问题

  1. 单例形式有几种写法
  2. 饿汉式怎么确保线程安全
  3. 类加载的进程都有什么,能介绍下每个阶段都做了什么嘛
  4. volatile都有什么效果,什么是指令重排序
  5. 静态内部类单例是怎么做到线程安全的。它的缺陷是什么
  6. 为什么说枚举占内存,为什么枚举不能被反射

看到这儿,你或许会说,这几个问题有的和单例也不要紧啊!确实,这儿有些问题和单例是不要紧,可是有没有一种或许,面试官压根就不是想单纯的问你单例的写法,这些单例引申出的常识点,才是他实在的意图

单例的写法

首先是上面的第一个问题,单例的写法。 关于单例的写法,这儿不再多做介绍了,网上太多的文章了。这儿直接说答案,一共5种写法。这儿贴一篇废物科普文单例形式,今日你用了嘛

直入主题

下面咱们每种写法,都来看一下它的优缺陷,以及运用时或许碰到的问题。

1. 饿汉式

咱们先来看一下,最简略的单例写法饿汉式。
饿汉式的长处是:写法简略,且线程安全。那么它的缺陷是什么呢。看下面的写法就知道了。对比其他文章,我往里加了一些比较极点的代码,便利了解。

/**
 * @author jtl
 * @date 2021/7/20 11:14
 * 饿汉式单例
 * 长处:线程安全的
 * 缺陷:由于类加载时就会创立目标。会形成内存糟蹋。
 */
public class HungrySingle {
    private static final HungrySingle S_HUNGRY_SINGLE = new HungrySingle();
    // 只需加载HungrySingle类,就会创立50M内存的数组。
    private byte[] aaa = new byte[1024*1024*50];
    private HungrySingle(){
    }
    public static HungrySingle getInstance(){
        return S_HUNGRY_SINGLE;
    }
    // 调用test办法时,运用aaa数组
    public void test(){
        for (byte data:aaa){
            data = 127;
        }
    }
    public static int info(){
        return 2;
    }
}

饿汉式缺陷:

上面的比如中,有点极点,可是确很好的表现了饿汉式的缺陷。

  1. 上面的比如中有一个aaa数组,它会在test()办法中被运用。
  2. 当我调用HungrySingle.info();这个静态办法时。会履行HungrySingle的类加载
  3. 履行类加载时,会履行private static final HungrySingle S_HUNGRY_SINGLE = new HungrySingle();会创建目标
  4. 由于创立目标,会创立aaa数组,即便我现在没有调用test,不需要运用这个数组
  5. 终究饿汉式单例,就会由于调用了一个static办法而创立目标,从而请求不必要的内存,导致糟蹋功用

面试官或许的问题:

由于类加载时就会创立目标,会形成内存糟蹋。

比如比如中,只需创立HungrySingle的目标,会平白无故的创立50m内存的数组。那么什么时分会创立目标呢?由于S_HUNGRY_SINGLE是static 润饰的,所以一履行类加载就会创立目标。

这儿就或许就包含面试官想考你的常识点了:

  1. 你能描绘下类加载的进程嘛?
  2. 什么时分会履行类加载?

什么是类加载

首先饿汉式触及到的第一个常识点,便是类加载
类加载是什么呢,当咱们运用一个类的时分,首先要做的是把这个类,也便是咱们java文件编译出的.class文件,加载到虚拟机之中。
类加载分为5个根本进程:

  1. 加载:将class文件二进制字节省的办法加载到内存中
  2. 验证:验证字节码的安全性,以防有人篡改字节码
  3. 预备:静态变量默许初始值(int类型初始值为0,引证类型为null等),static final润饰的常量在这一步直接赋值(static final润饰的根本数据类型会将成果编译到字节码中)
  4. 解析:将符号引证转换为直接引证
  5. 初始化:履行static代码块,初始化static变量,该进程即为clinit。

经过类加载之后,该类的相关数据会被保存在,办法区中存放类型信息的位置。另外,类的生命周期还包含运用和卸载,此处讲的是类的加载进程,所以没有把这两个生命周期写入。

什么时分会履行类加载呢?

当JVM履行HungrySingle这个类的相关代码的时分,第一件作业便是去查看办法区中是否存在该类的信息。假如存在,证明现已加载过HungrySingle,假如不存在,那么就履行HungrySingle的类加载。

以上面代码为例:一旦加载该类,由于static final HungrySingle S_HUNGRY_SINGLE = new HungrySingle(); 的原因就会在类加载的初始化阶段创立目标。又由于创立目标的时分会创立数组byte[] aaa,这样就形成了功用糟蹋。

触及的常识点:

  1. 什么时分会履行类加载
  2. 类加载的进程
  3. static润饰的变量什么时分进行赋值初始化

不引荐的原因:

类加载时会创立该类目标,或许会无意中创立一些占用内存的目标或者数组,形成功用糟蹋。


懒汉式

懒汉式单例,能够防止上面饿汉式中,调用static办法时就创立目标的这个缺陷。一同经过添加synchronized关键字,确保了线程的安全性

下面是懒汉式的写法

/**
 * @author jtl
 * @date 2021/7/20 11:41
 * 懒汉式
 * 长处:不会形成内存糟蹋
 * 缺陷:不加synchronized 会形成线程安全问题
 *       加 synchronized 会形成功用糟蹋。
 *
 */
public class LazySingle {
    private static LazySingle sLazy ;
    private LazySingle(){
        System.out.println("懒汉式:"+Thread.currentThread().getName());
    }
    public static synchronized LazySingle getInstance(){
        if (sLazy==null){
            sLazy = new LazySingle();
        }
        return sLazy;
    }
}

懒汉式的缺陷

懒汉式经过synchronized润饰getInstance办法,来确保了,多个线程一同调用getInstance时,不会在内存中创立多个LazySingle目标,即确保了它的线程的安全性。可是由于每次调用都会获取锁,所以会形成功用上的损耗。

面试官的切入点

面试官或许在问你懒汉式的一同,让你介绍一下synchronized关键字的相关常识点。 一旦说到synchronized这个关键字,那就不是一篇文章能够讲清楚的,这儿只提一下,他或许触及到的常识:

  1. 在多线程中,经过锁不同的目标,来确保线程的履行次序。
  2. 锁的目标,能够是目标,办法,以及class类。
  3. 在字节码中,经过ACC_SYNCHRONIZED,以及monitorenter和 monitorexit来完成。
  4. 锁的四种状态,无锁,偏向锁,轻量级锁,重量级锁
  5. 怎么完成上述这四种锁(这四种锁究竟是怎么完成的)
  6. 锁的晋级进程(markword中怎么记录偏向锁,轻量级锁,重量级锁)

这儿着重介绍下字节码中怎么完成,以及锁的晋级进程(文章最后的图片)。

这个是上面懒汉式的字节码,能够看到synchronized润饰办法时,在字节码中变成了ACC_SYNCHRONIZED标记。后边还会看到synchronized润饰目标时,字节码中变成monitorenter和monitorexit字节码指令。

  public static synchronized single.LazySingle getInstance();
    descriptor: ()Lsingle/LazySingle;
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #33                 // Field sLazy:Lsingle/LazySingle;
         3: ifnonnull     16
         6: new           #34                 // class single/LazySingle
         9: dup
        10: invokespecial #39                 // Method "<init>":()V
        13: putstatic     #33                 // Field sLazy:Lsingle/LazySingle;
        16: getstatic     #33                 // Field sLazy:Lsingle/LazySingle;
        19: areturn
      LineNumberTable:
        line 21: 0
        line 22: 6
        line 25: 16
      StackMapTable: number_of_entries = 1
        frame_type = 16 /* same */
}
SourceFile: "LazySingle.java"

懒汉式触及的常识点:

  1. 懒汉式怎么确保线程安全
  2. synchronized相关常识

不引荐的原因:

每次获取目标时,都要获取目标锁。糟蹋功用。


两层检查

假如在面试进程中,被面试官问到两层检查单例。那么volatile必定会成为一个考点。 咱们先看一下下面这段代码

/**
 * @author jtl
 * @date 2021/7/20 11:46
 * 两层检查形式单例
 * 长处:线程安全
 * 缺陷:反射能够损坏单例
 * 留意:需加volatile,由于 new操作自身不是线程安全的。重排序会呈现问题
 */
public class DCLSingle {
    private static volatile DCLSingle sDCLSingle;
    private int price = 8000;
    private DCLSingle() {
        System.out.println("两层检查形式:" + Thread.currentThread().getName());
    }
    public static DCLSingle getInstance() {
        if (sDCLSingle == null){
            synchronized (DCLSingle.class){
                if (sDCLSingle ==null){
                    sDCLSingle = new DCLSingle();
                }
            }
        }
        return sDCLSingle;
    }
}

面试的切入点

DCL(Double Check Lock),这个形式其实是引荐的一种形式。它既确保了线程安全性,又确保了延时加载(创立目标)。可是这儿有一个关键字volatile,当面试官问你DCL的时分,就意味着他或许要问你下面几个问题:

  1. 对volatile熟悉嘛?
  2. 这儿的volatile起到了什么效果?
  3. volatile还有其他的功用吗?

在讲volatile前,想问大家一个问题,当咱们履行new关键字,创立一个目标的时分。在JVM中或者说,在字节码层面究竟是什么样的? 假如你跟我说,我这天天都在写功用,谁会介意字节码什么样啊。那么也没问题,你没看过,那我给你预备好了。下面这段便是上面那段代码的字节码。让咱们一同看一下。

下面是上面DCL单例中getInstanch办法的字节码。让咱们看下当履行new目标操作的时分。字节码中究竟都有哪些指令。

{
  public static single.DCLSingle getInstance();
    descriptor: ()Lsingle/DCLSingle;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #41                 // Field sDCLSingle:Lsingle/DCLSingle;
         3: ifnonnull     37
         6: ldc           #42                 // class single/DCLSingle
         8: dup
         9: astore_0
        10: monitorenter                      // monitorenter指令获取锁
        11: getstatic     #41                 // 将sDCLSingle压入操作数栈
        14: ifnonnull     27
        17: new           #42                 // ① 请求内存创立目标
        20: dup
        21: invokespecial #47                 // ② 履行结构办法 Method "<init>":()V
        24: putstatic     #41                 // 将sDCLSingle压出操作数栈
        27: aload_0                           //  赋值给sDCLSingle
        28: monitorexit                       // monitorexit指令释放锁
        29: goto          37
        32: astore_1
        33: aload_0
        34: monitorexit
        35: aload_1
        36: athrow
        37: getstatic     #41                 // Field sDCLSingle:Lsingle/DCLSingle;
        40: areturn
      Exception table:
         from    to  target type
            11    29    32   any
            32    35    32   any
}

指令重排序:

这儿要先遍及一个常识,什么是指令重排序
指令重排序:编译器在不改动单线程程序的履行成果的前提下,能够将指令进行重新排序,以提高履行功率。

正常履行次序

咱们从字节码中看到三个操作,①②③,正常状况下,字节码中咱们代码的履行次序是:

  1. ①请求内存创立目标,此时该示例中的price只赋了默许值0
  2. ②履行结构办法,此时a会赋值成代码中的8。
  3. ③将sDCLSingle实例目标指向①中创立的内存,这就意味着此时的sDCLSingle目标不为null

上面的履行次序也是正常的默许的履行次序。

重排序后的履行次序

可是有正常的履行次序,就意味着必定会有不正常的履行次序。

假如sDCLSingle中不运用volatile润饰的状况下,编译器就或许为了优化,从而进行指令重排序。次序就或许从①②③,变成①③②

假设呈现极点的状况,指令变成了①③②。一同呈现了两个线程A和B一同履行getInstance操作。 第一个A线程在履行new目标时,由于指令重排序,正好履行到了①③操作。这时由于③操作给sDCLSingle赋了值,导致sDCLSingle目标不为null,可是由于没有履行②,所以sDCLSingle目标中的price=0。恰巧这时的线程B履行了getInstance办法。由于sDCLSingle不为null,所以线程B直接获取了,尚未履行初始化操作的sDCLSingle目标。原本price为8000,可是由于该目标还没有履行操作②没有设置初始值,线程B中的price为0。假如这是一个付款操作,那就变成了原本8000块的商品,变成了0元购,这归于妥妥的事故现场啊。

为了防止这种状况,咱们的volatile就出场了,volatile的两大特性:

  1. 禁止指令重排序
  2. 确保内存的可见性

禁止指令重排序,这点能够了解为,为了确保上述代码在编译时次序永远是①②③,而不会变成①③②。禁止编译器进行指令重排序,以防止上述的状况。

volatile的可见性,这儿不做过多描绘,感兴趣的同学能够查看下小虎牙童鞋的这篇volatile的文章或者上B站看下马士兵教师的多线程的免费课程。讲的比较具体。

DCL触及的常识点:

  1. volatile的相关常识
  2. 什么是指令重排序

volatile常识的图解

面试官,你要跟我聊单例?那我可有话说了

静态内部类

静态内部类这种单例,它之所所以线程安全的。原因便是JVM加载类的时分是线程安全的,咱们在调用getInstance办法时,会加载Inner内部类,由于JVM确保了同一时间只能有一个线程加载相同的类,所以静态内部类是线程安全的。

当咱们调用HolderSingle.test1()办法时,由于不会创立HolderSingle目标,因而也不存在饿汉式单例的缺陷。

/**
 * @author jtl
 * @date 2021/7/20 11:49
 * 静态内部类单例
 * 长处:线程安全,由于类加载时是线程安全的
 * 缺陷:反射能够损坏单例
 */
public class HolderSingle {
    private HolderSingle(){
        System.out.println("静态内部类单例:"+Thread.currentThread().getName());
    }
    public static HolderSingle test1() {
        System.out.println( "---测验代码---");
    }
    public static HolderSingle getInstance() {
        return Inner.sHolder;
    }
    private static class Inner{
        private static final HolderSingle sHolder = new HolderSingle();
    }
}

可是静态内部类,也有一个缺陷便是,能够经过反射来获取其实例。请看下面的代码。

/**
 * @author jtl
 * @date 2021/7/20 14:29
 * 单例形式测验Test
 */
class Client {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 静态内部类,经过反射获取实例
        // 获取HolderSingle类的结构器
        Constructor<HolderSingle> holderConstructor = HolderSingle.class.getDeclaredConstructor();
        // 获取权限,能够履行private办法
        holderConstructor.setAccessible(true);
        // 履行结构器,创立目标
        HolderSingle holder = holderConstructor.newInstance(null);
        System.out.println("单例目标:" + holder+"---hashCode:"+holder.hashCode());
    }
}

面试官,你要跟我聊单例?那我可有话说了
经过运行上述代码,能够输出图片中的语句。因而能够证明,咱们能够经过反射获取静态内部类单例的目标实例。这与咱们单例的概念不符合。因而这也算是他的一个缺陷。不过话说回来,这只是较真的一种行为,毕竟都现已运用单例了,那咱们必定不会经过反射来获取实例。

静态单例的考点:

  1. 咱们能够经过反射,获取静态内部类单例目标实例。即反射能够履行私有办法
  2. JVM自身会确保,类加载时的线程安全性

枚举单例

枚举单例,相对于上面几种,或许是知道的比较少的一种单例写法了。比较于前面几种办法,它的长处是,完美解决了反射获取目标实例的这一行为。

/**
 * @author jtl
 * @date 2021/7/20 11:57
 * 枚举单例形式,可防反射
 */
enum EnumSingle {
    INSTANCE;
}

假如咱们运行下面这段代码,想经过反射来获取枚举的目标实例,会呈现下图这种状况。

/**
 * @author jtl
 * @date 2021/7/20 14:29
 * 单例形式测验Test
 */
class Client {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 测验枚举类单例,无法经过反射获取, Cannot reflectively create enum objects
        Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);//枚举的结构函数是有参的
        enumSingleConstructor.setAccessible(true);
        EnumSingle enumSingle = enumSingleConstructor.newInstance(null);
    }
}

面试官,你要跟我聊单例?那我可有话说了

进击的面试官

这时分,聪明的面试官或许就要开启追问形式了:

  1. 你能不能跟我说说,为什么枚举无法经过反射获取实例呢?
  2. 枚举的本质究竟是什么呢
  3. 枚举的缺陷是什么,为什么功用优化时,会主张运用注解来替代枚举

要答复第一个问题,看完下面这段,反射的相关代码你就该知道答案了

public final class Constructor<T> extends Executable {
    private ConstructorAccessor acquireConstructorAccessor() {
        Constructor<?> root = this.root;
        ConstructorAccessor tmp = root == null ? null : root.getConstructorAccessor();
        if (tmp != null) {
            constructorAccessor = tmp;
        } else {
            // 类型为枚举时,直接抛反常
            if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
            tmp = reflectionFactory.newConstructorAccessor(this);
            if (VM.isJavaLangInvokeInited())
                setConstructorAccessor(tmp);
        }
        return tmp;
    }
}

看完上面的代码,你就知道为啥Enum不能反射了吧,不是它不想,而是Java它根本不允许啊。

紧接着让咱们看下,Enum的实在面貌: 让咱们看下,上述代码的字节码,你会惊讶的发现,好好的一个枚举,在编译之后变成了一个承继了Enum的一个class类。

openjdk version "19.0.1" 2022-10-18
OpenJDK Runtime Environment (build 19.0.1+10-21)
OpenJDK 64-Bit Server VM (build 19.0.1+10-21, mixed mode, sharing)
haohao@192 single % javap EnumSingle.class
Compiled from "EnumSingle.java"
final class single.EnumSingle extends java.lang.Enum<single.EnumSingle> {
  public static final single.EnumSingle INSTANCE;
  public static single.EnumSingle[] values();
  public static single.EnumSingle valueOf(java.lang.String);
  static {};
}

让咱们再来看一下Enum这个抽象类,究竟是何方神圣。这便是为什么,咱们在写枚举的时分能够直接调用name等办法。

public abstract class Enum<E extends Enum<E>>
        implements Constable, Comparable<E>, Serializable {
    private final String name;
    private final int ordinal;
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    public final String name() {
        return name;
    }
    public final int ordinal() {
        return ordinal;
    }
    public String toString() {
        return name;
    }
    public final boolean equals(Object other) {
        return this==other;
    }
}

枚举中的小心思: 让咱们将目光转移回编译出的Enum代码。细心的小伙伴,或许现已发现了。这个INSTANCE是一个static final润饰的目标啊。这是不是就意味着,每有一个枚举就代表了在编译之后会呈现一个目标。这当然比注解占内存了。

这便是为什么Google引荐运用注解来取代枚举的原因。由于,每一个枚举编译之后都会生成一个实例目标。而反观注解,它的根本类型是什么,他在内存中就占多少内存。

面试官,你要跟我聊单例?那我可有话说了

回忆下枚举单例常识点

假如面试官说到枚举单例的话,那么他或许想跟你聊的不是枚举单例,而是枚举的本质,大约便是下面这几个问题:

  1. 枚举单例比较于静态类单例的长处是什么嘞
  2. 枚举的本质是什么
  3. 为什么功用优化里,会呈现注解替代枚举的说法,其原因是什么。

单例总结

简简略单的五种单例写法里,暗藏了多少杀机,看完这篇文章之后,我想面试官应该再也不想问你单例问题了。当然也有例外,假如他非要让你讲一下,synchronized在硬件方面是怎么完成的。听我一句劝,快跑,这个面试官或许是派大星,由于他大约率不是个正常人>.<

话说回来,再看下,单例都触及哪些常识点:

  1. JVM加载类的机制
  2. volatile 原理
  3. synchronized 相关常识
  4. 静态内部类是怎么确保线程安全的
  5. 反射机制,枚举的实在面貌,以及枚举耗费内存的原因

面试中单例的问题

现在你能答复下图中的问题了吗,假如全能答复上来的话,那么祝贺你。假如还有一些疑问的话,你或许需要再看一遍 >.<

面试官,你要跟我聊单例?那我可有话说了

2022年最后想说的

今日是2022年12月31日,行将过去的这一年里,咱们阅历了太多,俄乌战役经济隆冬,互联网大批裁员,疫情解封全员小洋人。在这种状况下,咱们只能不断的学习,来充实自己。期望在新的一年里,大家都能找到更好的作业,生活的愈加开心。

规划形式连接

23种规划形式的相关代码,现在还差几种有时间会补全。Github23种规划形式的demo

锁晋级的进程

末尾引证网上的一张锁膨胀进程的图片,感兴趣的能够看一下。

面试官,你要跟我聊单例?那我可有话说了