自学内容网 自学内容网

【多线程】单例模式

🏀🏀🏀来都来了,不妨点个关注!
🎧🎧🎧博客主页:欢迎各位大佬!
在这里插入图片描述

1. 什么是单例模式

单例模式是一种经典的设计模式它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。 单例模式主要用于控制对某些共享资源的访问,例如配置管理器、连接池、线程池、日志对象等。
这里我们提到了设计模式,我们就先介绍下什么是设计模式,下过棋的同学应该都知道什么是棋谱,就是一些固定的下棋套路,这些下棋套路可能不算特别好,但也不会特别差,或者经常玩云顶的小伙伴也知道阵容这个说法,我刚开始接触云顶的时候不怎么会玩,就会去网上搜索各种阵容套路,这其实也是一种设计模式,它说不上特别好,但也不会特别差,不会让我们老八出门。
设计模式:相当于软件开发中的“棋谱”,针对很多常见的 "问题场景"总结出的一些固定的套路. 按照这个套路来实现代码, 不会吃亏。
我们今天要介绍的单例模式就是其中的一种设计模式,下面我们介绍两种最常见的单例模式,分别是饿汉模式懒汉模式

1.1 理解单例模式

在介绍之前我们先举个生活中的小例子来大致了解下什么是饿汉模式什么是懒汉模式:对于吃饭后什么时候洗碗这件事,小丁和小万有着截然不同的观点,小丁本着 非必要不洗碗(懒汉模式) 的观点,每次吃完饭之后就会把碗放在一旁去干其他事了,等下次什么时候吃饭了再来洗碗。而小万就不一样了,她很勤快,每次吃完饭后就急迫着去洗碗(饿汉模式),不管下次啥时候吃饭,先把碗洗好再说。

1.2 单例模式的特点

在具体写代码实现单例模式之前我们需要想了解单例模式的特点:

  1. 私有构造方法:确保外部代码不能通过构造器创建类的实例。
  2. 私有静态实例变量:持有类的唯一实例。
  3. 公有静态方法:提供全局访问点以获取实例,如果实例不存在,则在内部创建。

2. 饿汉模式

饿汉式单例在类加载时就急切地创建实例,不管你后续用不用得到,这也是饿汉式的来源,简单但不支持延迟加载实例。
具体代码实现:

//饿汉模式
class Singleton{
    //唯一实例的本体
    private static Singleton singleton = new Singleton();

    //公有获取到实例的方法
    public static Singleton getSingleton() {
        return singleton;
    }

    //将构造方法封装为私有的,外部不能创建新的对象
    private Singleton() {

    }

}
public class ThreadDemo18 {
    public static void main(String[] args) {
       Singleton s1 = Singleton.getSingleton();
       Singleton s2 = Singleton.getSingleton();
       //s1 和 s2 为同一对象,打印true
        System.out.println(s1 == s2);
    }
}

这里我们运行可以看到s1和s2对象是一样的:
在这里插入图片描述
在饿汉模式下,即使有多个线程同时调用getSingleton()方法也不会有线程安全的问题,这是因为这里只涉及到读取数据的情况,但多线程下,懒汉模式可能就无法保证创建对象的唯一性了,下面我们就来重点介绍下懒汉模式。

3. 懒汉模式

类加载的时候不创建实例. 第一次使用的时候才创建实例.

3.1 单线程下的懒汉模式

在写具体的单线程的懒汉模式之前我们需要先清楚懒汉模式,既然叫懒汉,突出一个字——懒,即非必要,不加载。 那代码也就很理所当然了,只有在你第一次调用该类的时候,才会去创建对象,我们就可以在对外提供的公有的非静态方法中进行判断,当该对象为空的时候才去创建对象,代码如下:

class  SingletonLazy {
   private static SingletonLazy instance = null;

  public static SingletonLazy getSingletonLazy() {
      if (instance == null) {
          instance = new SingletonLazy();
      }
      return instance;
  }
  private SingletonLazy() {
  
  }
}
public class ThreadDemo19 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getSingletonLazy();
        SingletonLazy s2 = SingletonLazy.getSingletonLazy();
        System.out.println(s1 == s2);
    }
}

运行结果:
在这里插入图片描述

3.2 多线程下的懒汉模式

为什么会有多线程下的懒汉模式,肯定是因为单线程下的懒汉模式在多线程的环境中会出现线程安全的问题嘛,这个我们可以从上面单线程下的懒汉模式在多线程环境下可能会出现的问题进行解释,如下图:

在这里插入图片描述
在之前的线程安全文章中,我们提到过,线程不安全的罪魁祸首是CPU的调度是无序的,是抢占式执行的,所以在上述代码逻辑中,就会出现以下安全问题:t1线程判断instance为空,会new出来一个对象,接下来,CPU调度资源分配给了t2线程,此时t2线程也判断instance为空,会new出来一个对象,这样就会导致new出来了两个对象。到这里很多人可能不以为然,不就多new出来一个对象嘛,多大点事。但如果这个对象特别大呢,比如100G,这样就对资源造成了很大的消耗,并且也不满足我们的初心一个类只有一个实例对象。
那如何解决呢,在之前的线程安全的文章中我们也提到过类似的问题,那就加锁嘛,将if判断instance为空和new对象这个操作进行加锁保证它是一个原子性的操作就好了嘛,那代码就很理所应当了,如下:

class SingletonLazy{
    private static SingletonLazy  singletonLazy = null;
    

    public static SingletonLazy getSingletonLazy() {
            synchronized (SingletonLazy.class) {
               if (singletonLazy == null) {
                   singletonLazy = new SingletonLazy();
               }
            }

        return singletonLazy;
    }
    
    private  SingletonLazy() {

    }
}

看到这里,很多人肯定就以为就完事了,但其实我们需要知道一个事,加锁其实是很低效的事情,加锁可能就会涉及到线程的阻塞等待,所以我们希望的是非必要,不加锁。 再看我们上述的代码,每个线程来调用这个getSingletonLazy()方法来获取单例对象的时候都需要进行加锁,这就导致了一个问题,当很多线程同时来获取这个单例对象的时候,当一个线程t1获取到锁之后,其他线程只能进行阻塞等待,当t1线程执行完之后,接着一个t2线程获取到锁,其他线程也需要阻塞等待,非常的低效。这就类似我们排队买吃的一样,当前面的人在买早餐的时候,我们只能在后面慢慢排队等待。

3.2.1 多线程下的懒汉模式(优化)

我们回想一下我们对该地方进行加锁的初心,是在第一次实例化这个对象的时候,由于CPU的调度,可能会导致两个线程都会判断instance为空,从而实例化两个对象出来,那我们就可以在这里做个小优化了:只有当instance对象为空的时候,即还没有创建过该对象的时候我们才进行加锁, 因为只能在第一次实例化该对象的时候会出现线程安全的问题,后面有了该对象之后我们只涉及到读取这个对象的操作不涉及到修改操作就不会产生线程安全的问题。
代码实现如下:

class SingletonLazy{
    private static SingletonLazy  singletonLazy = null;

    public static SingletonLazy getSingletonLazy() {
        if (singletonLazy == null) {
            synchronized (SingletonLazy.class) {
               if (singletonLazy == null) {
                   singletonLazy = new SingletonLazy();
               }
            }

        }
        return singletonLazy;
    }

    private  SingletonLazy() {

    }
}

3.2.2 多线程下的单例模式(双重检验锁)

看过我之前写的线程安全的文章的小伙伴应该都知道,上面new对象的操作也可能会涉及到指令重排序的问题,以此,我们在这里也需要对new对象这个操作进行下改进。对instance对象加上volatile关键字修饰即可。
new对象操作大致分为以下三步:

  1. 给该对象分配内存空间
  2. 调用构造方法,进行初始化
  3. 将内存地址指向引用地址

代码如下:

class SingletonLazy{
    volatile private static SingletonLazy  singletonLazy = null;

    public static SingletonLazy getSingletonLazy() {
        if (singletonLazy == null) {
            synchronized (SingletonLazy.class) {
               if (singletonLazy == null) {
                   singletonLazy = new SingletonLazy();
               }
            }

        }
        return singletonLazy;
    }
    private  SingletonLazy() {

    }
}

这也是面试官经常会考的问题:让你手写一个单例模式,并解释下双重检验锁使用单例模式的原理。
关键点解释
volatile关键字
确保多个线程能够正确处理instance变量。volatile禁止了指令重排序优化,保证了在多线程环境下变量的可见性。即当一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。
双重检查
第一次检查是在方法级别进行的,如果instance不为null,则直接返回实例,避免不必要的同步开销。
第二次检查是在同步块内部进行的,这是为了防止多个线程同时进入同步块并创建多个实例。

今天的分享就结束了,感谢支持!


原文地址:https://blog.csdn.net/weixin_62848751/article/details/140670408

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!