5.Java设计模式-创建型模式-单例模式-懒汉式-“双重检查锁+volatile“实现

发布时间:2024年01月13日


1.懒汉式

懒汉式:跟饿汉式在类加载时创建不一样,懒汉式是在我们第一次使用时才创建

懒汉式,顾名思义,比较懒,没事儿就不会创建

2.懒汉式-"双重检查锁+volatile"实现

在这里插入图片描述

  • 懒汉式用"双重检查锁+volatile"的实现方式如上图,重点代码我框了起来,并进行了编号
  • 下面的步骤有点多,没办法,双重检查锁+volatile确实不好讲,细节也多,建议大家多看几遍,慢慢梳理流程
  • 按②③④⑤的顺序梳理一下整体逻辑
    • step 1:假设现在有线程1线程2同时调用了getInstance方法,他们都走到了,由于INSTANCE并没有被创建,所以处的if判断,线程1线程2都为true,他们都可以走到
    • step 2:假设线程1线程2同时走到了,此时就会发生锁的竞争,这里我们假设是线程1抢到了锁,能接着往下执行,线程2则在等待,等待线程1释放了锁后再执行后续代码
    • step 3:线程1走到的时候,想必大家都有疑问,咋又来判断if(INSTANCE == null)呢?后面再给大家解答,我们先接着往下走
    • step 4:线程1进行if判断的结果肯定为true,所以线程1能走到,这里敲重点,让我们的视线看向,我们在INSTANCE前加了volatile关键字,这个关键字的作用我先不讲,我们先把流程梳理完后再来讲
    • step 5:线程1走完后,INSTANCE就会被创建出来了,这时候线程1就可以释放锁,拿着创建好的INSTANCE退出getInstance方法去执行自己的业务逻辑
    • step 6:在线程1从执行到执行完的这个期间内,可能会有其他的线程例如线程3、线程4、线程5也调用了getInstance方法,这三个新的线程肯定都会和线程2一样卡在等待线程1释放锁
    • step 7: 当线程1释放了锁,也就是创建好INSTANCE后,线程2、线程3、线程4、线程5就会抢锁,无论它们之间谁抢到了锁,都会来到,这个时候,INSTANCE已经不为空了,他们都会拿着线程1实例化好的INSTANCE直接返回了,就不会再重新new Singleton()了,这也就解答了step 3提出的问题,即为什么还需要一个if判断。②③④就构成了双重检查锁
    • step 8:在线程1执行完后,如果有新的线程例如线程6调用了getInstance方法,那么线程6走到就为false,直接拿着线程1创建好的INSTANCE返回,即后续所有新的线程调用getInstance就都不会走到了,也就不用加锁和释放锁了,也就解决了上一章在方法上加synchronized导致频繁加锁和释放锁以及锁的粒度大的问题
    • step 9:最后我们来看为啥要在INSTANCE前加volatile关键字,大家看向,其实new一个对象的过程总共涉及三步,并不是一蹴而就的
      • 1:分配内存空间,实例化对象(这个时候对象已经有了,但是对象里面的成员变量存的都是默认值)
      • 2:初始化对象,即调用构造方法为成员变量赋实际值
      • 3:将内存空间地址赋值给INSTANCE
    • 我们先假设Singleton类中有int a; int b; double c;这三个成员变量,然后再往下看
    • step 10:正常创建一个对象的步骤是1-2-3,如果不加volatile,可能会出现指令重排(指令重排是操作系统为了提高执行效率和资源的利用效率),指令重排可能会导致步骤变为1-3-2。在单线程的情况下,1-3-2没啥问题。如果是多线程情况下,就可能出现问题,假设没加volatile线程1执行到创建对象的时候发生了指令重排,即步骤变为了1-3-2,这个时候我们再假设线程1只执行完了1-3,2还没来得及执行,即已经将内存空间的地址赋值给了INSTANCE,但是INSTANCE里面的成员变量没有赋实际值,都还只是默认值,重点重点重点,这个时候突然来了一个线程10调用了getInstance方法,线程10走到的时候,if(INSTANCE== null)为false,线程10就拿到了这个没赋实际值的INSTANCE,若是去访问里面的成员变量,那么就会出现问题。所以加volatile是为了防止在多线程情况下创建INSTANCE的时候发生指令重排,让创建INSTANCE的步骤锁死为1-2-3,步骤锁死为1-2-3的话,只有在给成员变量赋好实际值之后,才会将内存空间地址赋值给INSTANCE

3.代码测试

public class TestDemo {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println("s1 == s2: " + (s1 == s2)); // s1 == s2: true
    }
}

4.总结

  • 优点
    • 线程安全
    • 支持延迟加载
    • 支持高并发
  • 缺点
    • 实现复杂

5.饿汉式和懒汉式总结

  • 饿汉式:如果系统中的单例大多采用饿汉式的话,那么这个系统启动的比较慢,因为要事先创建好所有的单例对象,资源的占用也比较多。但是这个系统的运行速度就要快些,因为需要的单例对象都已经创建好了,不需要在运行的时候创建
  • 懒汉式:如果系统中的单例大多采用懒汉式的话,那么这个系统启动的比较快,因为不需要的单例对象不必事先创建,资源的占用就比较少。但是这个系统的运行速度可能一开始就慢一些,因为单例对象要在运行途中创建

上一章和本篇我们讲了单例模式-懒汉式的两种实现方式,相信大家对懒汉式都有了一定的理解

好了,Java设计模式-创建型模式-单例模式-懒汉式就讲到这里了~

大家有什么问题或者建议,欢迎留言和讨论,虽做不到秒回(哈哈,没办法,要先工作 o(^ヘ^o),但会当天回复~

下一篇:6.Java设计模式-创建型模式-单例模式-静态内部类

文章来源:https://blog.csdn.net/Runnin_Athena/article/details/135498884
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。