• 多线程案例(1) - 单例模式


    目录

    单例模式

    饿汉模式

    懒汉模式 


    前言

    多线程中有许多非常经典的设计模式(这就类似于围棋的棋谱),这是用来解决我们在开发中遇到很多 "经典场景",简单来说,设计模式就是一份模板,可以套用。


    单例模式

    顾名思义,就是一个程序只能含有一个实例,有的场景中,希望一个类只能有一个对象,例如 JDBC 中的 DataSourse 实例就只需要一个。虽说程序员可以在编写代码时只给类创建一个对象,但是人毕竟没有机器靠谱,所以大佬们就设计了一套模板,按照模板来写代码,就不会出大的差错。

    实现单例模式的方法有两种:饿汉模式 和 懒汉模式。

    饿汉模式

    就是在类加载时,就创建实例。

    1. class SingleDemo{
    2. //在类加载的时候创建
    3. private static SingleDemo instance = new SingleDemo();
    4. //设置为private是为了防止在其他类中 new 一个实例,这样就不是单例模式了
    5. private SingleDemo(){ }
    6. //既然不能在外面创建实例,我们就要提供一个方法来得到这个唯一的实例
    7. public static SingleDemo getInstance() {
    8. return instance;//读取操作
    9. }
    10. }

    懒汉模式 

    顾名思义,就是在用到实例的时候再创建实例,与饿汉模式相比,提高了代码的效率。

    1. class SingleLazyDemo{
    2. private static SingleLazyDemo instance = null;
    3. //设置为private是为了防止在其他类中new一个实例
    4. private SingleLazyDemo(){ }
    5. public static SingleLazyDemo getSingleLazyDemo(){
    6. //第一次需要实例时,创建实例
    7. if(instance == null){
    8. //修改操作
    9. instance = new SingleLazyDemo();
    10. }
    11. return instance;//读取操作
    12. }
    13. }

    了解了什么是饿汉模式和懒汉模式,我有一个问题:上述两种写法,那种是线程安全的?

    我在前几篇博客中提到过,如果多个线程,同时修改同一个变量,此时就可能出现线程安全问题,所以显而易见,饿汉模式是线程安全的,它的方法中只涉及到读取操作,而懒汉模式是线程不安全的,它的方法中涉及到读取和修改操作。画个图理解一下:

     接下来,我们就来解决懒汉模式的线程安全问题。导致该模式出现线程安全的原因其实在图中已经体现出来了,这个一个非原子操作,针对这一问题,我们的解决方法就是加锁。

    1. class SingleLazyDemo{
    2. private static SingleLazyDemo instance = null;
    3. //设置为private是为了防止在其他类中new一个实例
    4. private SingleLazyDemo(){ }
    5. public static SingleLazyDemo getSingleLazyDemo(){
    6. synchronized (SingleLazyDemo.class){//使修改操作变成原子操作
    7. if(instance == null){//第一次需要实例时,创建实例
    8. instance = new SingleLazyDemo();
    9. }
    10. }
    11. return instance;
    12. }
    13. }

    此时,虽然懒汉模式的线程安全问题基本得到了解决,但是一旦这么写,后续每次调用 getInstance,都需要先加锁,而加锁的开销是很大的,只要涉及加锁,那么该代码就基本与"高性能"无缘了。实际上,我们的实例化对象的操作(即修改操作) 只是出现在第一次调用 getInstance 的时候。

    一旦对象被new出来了,后续的线程调用 getInstance 就没有必要加锁了,因为这时候只用读取操作,线程是安全的,所以我们还需要再添加一个条件:

    1. class SingleLazyDemo{
    2. private static SingleLazyDemo instance = null;
    3. //设置为private是为了防止在其他类中new一个实例
    4. private SingleLazyDemo(){ }
    5. public static SingleLazyDemo getSingleLazyDemo(){
    6. if(instance == null){//判断是否线程安全,要不要加锁
    7. synchronized (SingleLazyDemo.class){//使修改操作变成原子操作
    8. if(instance == null){//判断是否实例化
    9. instance = new SingleLazyDemo();
    10. }
    11. }
    12. }
    13. return instance;
    14. }
    15. }

    注意:这两个 if 虽然都是判断 instance 是否为 null, 但是第一个 if 实际上是借此判断线程是否要加锁,如果为null,就说明需要执行修改操作,线程不安全,要加锁,如果不为null,说明线程只要执行读取操作,线程安全,不要加锁。而第二个 if 则是借此判断是否要实例化对象。

    在经过上述修改后,此代码还有一个问题,这就涉及到了之前没细讲的指令重排序问题,该问题也是因为编译器优化导致的,编译器为了提高执行效率,可能会在逻辑顺序不变的情况下,调整原有代码的执行顺序。

    比如:new 操作,可以分成三步:1. 申请内存空间  2. 在内存空间上构造对象  3. 把内存的地址赋值给 instance 引用。在单线程中,new 操作可以按照 1 2 3 执行,也可以按照 1 3 2 执行,但是在多线程中,1 3 2 这样执行就可能导致线程安全问题。

    举个例子:当 t1 线程执行完 1 3 时,instance 就已经是非空了,这个时候 2 还没有执行,t2 线程就开始执行,因为这个时候 instance 非空,所以 t2 线程直接返回 instance,这个时候如果 t2 线程中的代码访问 Instance 中的属性和方法,那么就会出现BUG,因为 Instance 还没有构造对象。

    这个问题就需要使用 volatile 关键字来修饰 Instance,这样就可以保证 Instance 在 new 的过程中不会出现指令重排序的现象,下面是最终的代码:

    1. class SingleLazyDemo{
    2. private static volatile SingleLazyDemo instance = null;
    3. //设置为private是为了防止在其他类中new一个实例
    4. private SingleLazyDemo(){ }
    5. public static SingleLazyDemo getSingleLazyDemo(){
    6. if(instance == null){//判断是否加锁
    7. synchronized (SingleLazyDemo.class){//使修改操作变成原子操作
    8. if(instance == null){//判断是否实例化
    9. instance = new SingleLazyDemo();
    10. }
    11. }
    12. }
    13. return instance;
    14. }
    15. }
  • 相关阅读:
    Codeforces 398C LRU (概率dp)
    Rust中级教程:指针生态(引用、原始指针、智能指针)and内存操作(Stack、Heap)
    Molecular Plant | ChIP-seq+RNA-seq解析E2F转录因子在植物复制胁迫响应中的独特和互补作用
    15篇MyBatis-Plus系列集合篇「值得收藏学习」
    死锁是什么?死锁的字节码指令了解?
    static关键字和final关键字
    PL/SQL Some Advanced Fundamental
    地级市-空气流动系数数据-更新至2019(含10米风速、边界高度等)
    MongoDB 基础了解(三)
    uniapp数据可视化页面,引入uCharts开发(这里介绍一下图例的自定义开发,当数据上百条的情况处理)
  • 原文地址:https://blog.csdn.net/m0_74859835/article/details/132864279