• 【javaEE】多线程初阶(Part5单例模式)



    前言

    今天不学习,明天变垃圾!

    本文主要内容是:多线程案例中的【单例模式】。


    一、【单例模式】

    1. 单例模式和(工厂模式)都是常见的设计模式。
    2. 单例模式是校招中最常考的设计模式之一。
    3. 那么什么是设计模式?
      其实设计模式就是:针对某些问题场景,总结出一些固定的套路。
    4. 单例:就是单个实例instance(对象),即某个类有且只有一个实例
    5. 类似于前面学习过的jdbc编程,一个应用程序只有一份数据源DataSource就够了。
    6. 如果是口头协定只有一份就是不太靠谱的,所以:单例模式其实本质上就是借助编程语言自身的语法特性,强行限制某个类不能创建多个实例。
    7. static修饰的成员/属性就变成了类成员/类属性,也就是一个类只有一份,就相当于是“单例模式”。
    8. 类对象是通过JVM加载.class文件来的,此时的类对象在JVM中也是“单例”。换句话说,JVM针对某个.class文件只会加载一次,也就是只有一个类对象,类对象上面的成员(static修饰)也就只有一份。
    9. 方法里面是创建不了static修饰的变量的。

    二、单例模式【懒汉+饿汉模式】

    1. 饿汉模式:类加载的同时, 创建实例。
      懒汉模式:类加载的时候不创建实例, 第一次使用的时候才创建实例。

    2. 懒汉和饿汉模式,谁才是线程安全的?
      答:懒汉模式线程不安全,饿汉模式线程安全。
      ① 考虑某个模式是否线程安全,本质上是在考虑多个线程同时调用getInstance的时候是否会有问题。
      ② 饿汉模式获取实例getInstance的操作只是单纯的“读数据”,不涉及到修改,因为饿汉模式在类加载的时候就已经创建好实例对象了。
      ③ 懒汉模式获取实例getInstance的操作既涉及到读,又涉及到修改,此时线程就是不安全的。

    3. 那么如何修改懒汉模式的代码使得该模式下线程安全呢?
      答:加锁,把多个操作打包成一个原子操作。【注意锁对象:该类的类对象.class】

    我们主要是想要t2线程的读数据load操作在t1线程new实例之后,所以加锁是加在if之外。但是这种直接加锁的方式可能会导致后续线程安全的时候还在继续加锁;而加锁的开销其实还是挺大的,加锁可能会涉及用户态到内核态之间的切换,这里的切换成本是比较高的。

    1. 那么加锁如何在在需要的时候加,在不需要的时候就不加呢?
      答: 实例没有创建之前是线程不安全的,需要加锁;而在实例创建之后线程就是安全的,不需要加锁。
      因此:就在加锁的外层再加一层判定条件,符合条件才加锁。

    2. new操作本质上大致可以分为三个步骤:
      ① 申请内存,得到内存首地址;
      ② 调用构造方法来初始化实例
      ③ 把内存首地址赋值给instance引用

    • 在单线程的角度下,②③操作的顺序是可以进行调换的,执行谁效果都一样
    • 假设此处触发了指令重排序,并且按照① ③ ②的顺序执行,那就有可能在线程1在执行了①和③之后、执行②之前,线程2调用了getInstance方法,此时线程就是不安全的:因为调用了①③的时候其实并没有真正初始化实例对象,得到的是不完全对象,只是有内存,但是内存上的数据无效;而在线程2拿到实例的时候就会以为是已经创建了,实例对象为非空,并且可能会针对instance对象解引用操作来使用里面的属性/方法,这是不行的。

    ——其实上述问题就是指令重排序造成的问题

    1. 那么如何解决指令重排序带来的问题呢?
      答:办法就是:禁止指令重排序。那么如何禁止呢?就是使用volatile关键字,既能保证内存可见性(读、修改线程并发,但是其实细想是不会存在的:因为每个线程有各自工作的一套CPU寄存器,有各自的上下文),又能禁止指令重排序(避免得到不完全对象,内存数据无效)。

    2. 经典面试题
      ① synchronized锁加在哪里?
      ② 两层if是什么意思?
      ③ 为什么volatile不能少?

    3. 理解双重 if 判定 / volatile:
      ① 加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候。
      因此后续使用的时候, 不必再进行加锁了。
      ② 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了。
      ③ 同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile 。
      ④ 当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作。
      ⑤ 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了, 也就不会继续创建其他实例。
      ⑥ 这样会降低操作开销。

    4. 【单例的延伸】如何让单例模式反射安全以及序列化安全?(冷门八股,可以自行了解)


    三、代码参考

    其实面试题答案大多可以在参考代码中找到。
    Demo1-2

    注意要看每次提交所修改的!!不单只是看代码,看每次提交过程的代码!


    THINK

    1. 单例模式在校招中常考
    2. 单例模式的面试题:
      ① synchronized锁加在哪里?
      ② 两层if是什么意思?
      ③ 为什么volatile不能少?
    3. 代码实现
    4. 懒汉模式+饿汉模式
  • 相关阅读:
    三个“清洁代码”技巧将使您的开发团队效率提高 50%
    vue快速学习02、基础用法
    trino安装及使用
    芯驰科技与云驰未来达成战略合作,共同打造车规级信息安全产品
    径流数据整理
    FPGA解析B码----连载6(完结篇)
    Snipaste 提高十倍生产力工作效率,堪称最强神器
    Redis分布式锁
    PMP 考试需要注意哪些事项?
    AI系统ChatGPT源码+详细搭建部署教程+支持GPT4.0+支持ai绘画(Midjourney)/支持OpenAI GPT全模型+国内AI全模型
  • 原文地址:https://blog.csdn.net/weixin_54150521/article/details/127660280