• 多线程 - 单例模式


    v2-b6f7ee5f6c7a9e66581bda987710eb4f_b0213

    单例模式 ~~ 单例模式是常见的设计模式之一

    什么是设计模式

    你知道象棋,五子棋,围棋吗?如果,你想下好围棋,你就不得不了解一个东西,”棋谱”,设计模式好比围棋中的 “棋谱”.
    在棋谱里面,大佬们,把一些常见的对局场景,都给推演出来了,照着棋谱来下棋,基本上棋力就不会差到哪里去.
    同理,软件开发中也有很多常见的 “问题场景”, 针对这些问题场景, 大神们总结出了一些固定的套路, 按照这个套路来实现代码, 写的代码就不会太差.
    设计模式就是针对一些典型的场景,给出了一些典型的解决方案.

    单例模式

    单例模式 => 单个实例(对象)
    ~~ 通过巧用Java的现有语法,达成了某个类只能被创建出一个实例这样的效果,当我们不小心创建了多个实例,就会编译报错.

    场景: 很多场景广泛,比如JDBC中DataSource这样的类,其实就非常适合于使用单例模式.

    单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种
    注: 其实在Java里实现单例模式的方式有很多种,只是这两种最常见.

    饿汉模式

    类加载阶段,就把实例创建出来了(类加载是比较靠前阶段),这种效果,就给人一种"特别急切”的感觉,就像一个饿了很久的人,看到吃的,就会很急切,这种感觉就给它起了一个形象的名字,叫做“饿汉模式”,还有一个原因就是与后文讲解的“懒汉模式”相对应.

    class Singleton {
        // 在此处, 先把这个实例给创建出来了
        private static Singleton instance = new Singleton();
        // 被 static 修饰的 Singleton 这个属性和实例无关,而是和类有关
        /*
         * java 代码中的每个类,都会在编译完成后得到.class 文件.
         * JVM 运行是就会加载这个 .class 文件读取其中的二进制指令,并且在内存中
         * 构造出对应的类对象.(形如 Singleton.class) => 
         * */
        /*
         * 由于类对象 在一个 java 进程里,只是有唯一一份的
         * 因此类对象内部的类属性也是唯一一份了
         * */
    
        // 如果需要使用这个唯一实例, 统一通过 Singleton.getInstance() 方式来获取对象
        public static Singleton getInstance() {
            return instance;
        }
    
        // 为了避免 Singleton 类不小心被复制出多份来.
        // 把构造方法设为 private, 在类外面,就无法通过 new 的方式来创建这个 Singleton 实例了
        private Singleton() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    如何保证实例唯一的

    1. static这个操作,是让当前instance属性是类属性了.
      类属性是在类对象上的,类对象又是唯一实例的(只是在类加载阶段被创建出一个实例)
      • 注: 类属性和类对象是一 一对应的,即类对象如果是多个了,类属性也是就有多份了,此时类对象就不是单例的了.
    2. 构造方法是设为private.外面的代码中无法new.

    类加载阶段

    运行一个Java程序,就需要让Java进程能够找到并读取对应的.class文件,就会读取文件内容,并解析,构造成类对象…这一系列的过程操作,称为类加载.

    懒汉模式的实现

    这个实例并非是类加载的时候创建了,而是真正第一次使用的时候,才去创建(如果不用,就不创建了 => “懒”).
    注: 在计算机中,懒,往往是褒义词,勤快,才是贬义词 ~~ 从"效率"上考虑,懒汉模式比饿汉模式更胜一筹!!!

    懒汉模式-单线程版

    class SingletonLazy {
        private static SingletonLazy instance = null;
    
        public static SingletonLazy getInstance() {
            if (instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
    
        private SingletonLazy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上述写的饿汉模式和懒汉模式,如果在多线程环境下调用getInstance,是否是线程安全的?

    image-20231001135552693

    if (instance == null) { instance = new SingletonLazy(); } return instance;

    if (instance == null) {
           instance = new SingletonLazy();
       }
       return instance;
    
    • 1
    • 2
    • 3
    • 4

    image-20231001142343309

    刚才线程安全问题,本质是读,比较和写这三个操作不是原子的,这就导致了t2读到的值可能是t1还没来得及写的(脏读)

    懒汉模式-多线程版

    public static SingletonLazy getInstance() {
        synchronized (SingletonLazy.class) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注: 加锁了之后,确保了此时的读操作和修改操作是一个整体.
    image-20231001151835949

    懒汉模式-多线程版(改进)

     public static SingletonLazy getInstance() {
         synchronized (SingletonLazy.class) {
             if (instance == null) {
                 instance = new SingletonLazy();
            }
         }
         return instance;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上述代码就导致每次getlnstance都需要加锁(加锁操作是有开销的).
    问题来了: 真的需要每次加锁吗?
    是不需要的,这里的加锁只是在new出对象之前加上,是有必要的.
    一旦对象new完了,后续调用getlnstance,此时instance的值一定是非空的,因此就会直接触发return.
    相当于一个是比较操作,一个是返回操作,这两个操作都是读操作,此时不加锁也是OK的

    解决: 基于上述讨论,就可以给上面的代码加上一个判定:
    如果对象还没创建,才加锁;
    如果对象已经创建过了,就不加锁了.

    public static SingletonLazy getInstance() {
        if (instance == null) { // 此处不再是无脑加锁了而是满足了特定条件之后,才真正加锁.
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
         }
        return instance;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    解析: 如果这两个条件中间没有加锁,连续两个相同的 if 是没意义的.
    但是有了加锁,就不一定了,加锁操作可能会引起线程阻塞.当执行到锁结束,再执行到第二个if 的时候,
    第二个 if 和第一个 if 之间可能已经隔了很久的时间,沧海桑田.
    程序的运行内部的状态,这些变量的值,都可能已经发生很大改变了.
    注: 第一个 if 条件 负责判定是否要加锁, 第二个 if 条件负责判定是否要创建对象.这两个 if 条件的目的是完全不同的,只不过由于巧合,代码是一样的.

    上述懒汉模式的代码,还有内存可见性问题&指令重排序问题待解决!

    可见性问题
    假设有很多线程,都去进行getInstance,这个时候,是否就会有被优化的风险呢?
    (只有第一次读才是真正读了内存,后续都是读寄存器/cache)

    指令重排序问题

    instance new Singleton();
    拆分成三个步骤:
    1.申请内存空间.
    2.调用构造方法,把这个内存空间初始化成一个合理的对象.
    3.把内存空间的地址赋值给 instance 引用.

    正常情况下,是按照123这个顺序来执行的,但是编译器还有一手操作,指令重排序为了提高程序效率,调整代码执行顺序,123这个顺序就可能变成132.
    如果是单线程,123和132没有本质区别,但是多线程环境下,就会存在问题!!!
    假设t1是按照132的步骤执行的.t1执行到13之后,执行2之前,被切出CPU, t2 来执行(当t1执行完13之后, 在t2看来,此处的引用就非空了), 此时此刻, t2就相当于直接返回了instance引用并且可能会尝试使用引用中的属性. 但是由于t1中的2操作还没执行完呢, t2拿到的是非法的对象,还没构造完成的不完整的对象.

    volatile

    volatile有两个功能:

    1. 解决内存可见性
      2.禁止指令重排序

    完全体的单例模式(懒汉模式)代码

    class SingletonLazy {
        private volatile static SingletonLazy instance = null;// 1
    
        public static SingletonLazy getInstance() {
            if (instance == null) {// 2
                synchronized (SingletonLazy.class) {// 3
                    if (instance == null) {
                        instance = new SingletonLazy();
                    }
                }
            }
            return instance;
        }
    
        private SingletonLazy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  • 相关阅读:
    Google Earth 成长历程的15个小故事
    你和特斯拉CEO马斯克之间只相差一款优秀的计划管理工具飞项
    Docker安装启动Mysql
    Vue Jsp页面值绑定出现换行后台值导致syntaxerror 问题处理
    41. set()函数:将可迭代对象转换为可变集合
    【开发必备】单点登录,清除了cookie,页面还保持登录状态?
    CentOS 搭建k8s
    QMetaObject::invokeMethod与QThreadPool线程池使用
    【Linux】多线程
    基于GNS3的某省农科院网络组网规划方案设计
  • 原文地址:https://blog.csdn.net/m0_73740682/article/details/133466413