单例模式(Singleton Pattern)(一)

简介: 单例模式是Java中最简单的设计模式了,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

前言

iShot2022-12-05 00.30.04.png
单例模式是Java中最简单的设计模式了,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象,而这个类被称为单例类。

单例模式

单例模式也比较好理解,比如:每个人只能有一个真实的身份证号、每套房子也只能有一个房产证等类似的场景都是属于单例模式,那么翻译成专业术语就是:

  • 保证一个类只有一个实例(每个人只能有一个真实的身份证号)
  • 为该实例提供一个全局访问节点(通过身份证就能找到这个身份证号)

单例模式结构

image.png

单例模式之饿汉式

  • 大白话:顾名思义,饿汉嘛,就怕自己饿着。他总是把食物先准备好,什么时候需要吃了,他随时拿来吃,不需要临时去寻找食物。
  • 专业术语:饿汉式在一开始类加载的时候就已经实例化,并且创建单例对象,以后使用通过全局访问节点获取即可。
  • 注意:在类加载期间初始化静态实例,保证单例的创建是线程安全的。(如下方代码所示,instance在类加载时实例化,有JVM保证其线程安全)
  • 特点: 不支持延迟加载实例(懒加载) , 这种方式下类加载比较慢,但是获取实例对象比较快。
  • 缺点: 如果该单例对象足够大的话,初始化加载但是一直没有使用就会造成内存空间的浪费。
public class Singleton {

    /**
     * 私有构造方法,无法通过new Singleton()方式创建
     */
    private Singleton() {

    }

    /**
     * 在本类中创建私有静态的全局对象
     */
    private static Singleton instance = new Singleton();

    /**
     * 提供一个全局访问点,以便外部获取单例对象
     */
    public static  Singleton getInstance() {
        return instance;
    }
}

单例模式之懒汉式(线程不安全)

  • 大白话:顾名思义,他是一个懒汉,什么时候需要吃饭了,他就什么时候开始想办法搞点食物。
  • 专业术语:懒汉式一开始不会实例化,什么时候用就什么时候进行实例化。这个获取的方式就是全局访问节点。
  • 特点: 支持延迟加载实例,只有调用全局访问节点方法时才创建对象。
  • 缺点: 如果是多线程情况,会出现线程安全问题。
public class Singleton {

     /**
     * 私有构造方法,无法通过new Singleton()方式创建
     */
    private Singleton() {

    }
  
     /**
     * 在本类中创建私有静态的全局对象
     */
    private static Singleton instance;

    /**
     * 通过判断对象是否被初始化,来选择是否创建对象
     */
    public static  Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

为什么线程不安全?

假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程A在执行完if (instance == null) 后,线程调度机制将CPU资源分配给线程B,此时线程B在执行if(instance == null)时也发现单例类还没有被实例化,这样就会导致单例类被实例化两次。为了防止这种情况发生,需要对getInstance()方法进行同步处理。

单例模式之懒汉式(线程安全)

原理: 使用同步锁synchronized锁住 创建单例的方法 ,防止多个线程同时调用,从而避免造成单例被多次创建。

  • 所以,那么getInstance方法块只能运行在1个线程中。
  • 若该方法已在其他线程中运行,那么后续的线程试图运行该块代码,都会被阻塞而一直等待。
  • 在这个线程安全的方法里我们实现了单例的创建,保证了多线程模式下,单例对象的唯一性。
public class Singleton {

     /**
     * 私有构造方法,无法通过new Singleton()方式创建
     */
    private Singleton() {

    }

       /**
     * 在本类中创建私有静态的全局对象
     */
    private static Singleton instance;

    /**
     * 通过添加synchronized,保证多线程模式下的单例对象的唯一性
     */
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
由于给getInstance这个方法加了锁,所以这个方法的就是串行操作了,但是这个方法是获取单例的,所以调用肯定会很频繁,如果这个单例类偶尔会被用到,那这种方式还勉强能接受。但如果频繁用到,那相对应的频繁加锁、频繁释放锁导致并发度低等等问题,会导致性能瓶颈,这种实现方式就不太好了。

单例模式之双重校验

通过上文不难看出,饿汉式不支持延迟加载,而懒汉式又有性能问题不支持高并发。那么有没有一种既支持延迟加载、又支持高并发的单例实现方式呢?那就是双重校验方式。
原理: 1、在声明实例变量时使用volatile关键字,其作用是:

  - **保证变量的可见性**:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
  - **屏蔽指令重排序**:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。

2、将同步方法改为同步代码块,在同步代码块中使用二次检查,以保证其不被重复实例化,同时在调用全局访问节点方法时不进行方法级别的锁控制,提高效率。

public class Singleton {

    /**
     * 使用volatile关键字保证变量的可见性
     */
    private volatile static Singleton instance = null;

    /**
     * 私有构造方法,无法通过new Singleton()方式创建
     */
    private Singleton() {

    }

    /**
     * 全局访问节点
     */
    public static Singleton getInstance() {
        // 第一次判断,如果instance不为null,不进入竞争阶段,直接返回
        if(instance == null) {
            synchronized (Singleton.class) {
                // 获取锁之后再次进行判断
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么要进行二次判断?

image.png
这个校验是防止二次创建实例,假如有一种情况,当instance还未被创建时,线程T1调用getInstance方法,由于第一次判断instance==null,此时线程T1准备继续执行,但是由于资源被线程T2抢占了,此时T2也调用getInstance方法。由于instance并没有实例化,T2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后T2线程创建了一个实例instance。此时T2线程完成任务,资源又回到T1线程,T1此时也进入同步代码块,如果没有这个第二个if,那么,T1就也会创建一个instance实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。

在双重检查锁模式中为什么需要使用volatile关键字?

在java内存模型中,volatile关键字作用可以是保证可见性或者禁止指令重排。因为instance = new Singleton() ,它不是一个原子操作,在JVM中上述语句至少做了这3件事,如下:

  • 1、是给Singleton分配内存空间。
  • 2、开始调用Singleton的构造函数等,来初始化Singleton。
  • 3、将Singleton对象指向分配的内存空间(执行完这步instance就不是null了)。

而JIT编译器出于优化的目的,其实这1 -> 2 -> 3的顺序不能保证,它可能会存在指令重排序的优化,最终的执行顺序,可能是1 -> 2 -> 3,也有可能是1 -> 3 -> 2。
如果是1 -> 3 -> 2,那么在第 3 步执行完以后,instance就不是null了,可是这时第2步并没有执行,instance对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程T2进入getInstance方法,由于instance已经不是null了,所以会通过第一重检查并直接返回,但其实这时的instance并没有完成初始化,所以使用这个实例的时候会报错。

如下图所示:

image.png
使用了volatile之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在JDK5及以后的版本所使用的JMM中,在使用了volatile后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的读取到不完整对象的问题的发生。

小结

通过此文,我们了解了这些经常遇到的单例模式,但还有没有其他的方式呢?在日常开发中推荐使用哪种呢?下一篇文章将带你深入了解其他的单例创建方式,以及单例模式的破坏。

目录
相关文章
|
7月前
|
设计模式 存储 Java
Java设计模式:解释一下单例模式(Singleton Pattern)。
`Singleton Pattern`是Java中的创建型设计模式,确保类只有一个实例并提供全局访问点。它通过私有化构造函数,用静态方法返回唯一的实例。类内静态变量存储此实例,对外仅通过静态方法访问。
55 1
|
7月前
|
设计模式 Java
单例模式(Singleton Pattern)
单例模式(Singleton Pattern)
51 0
|
设计模式
设计模式3 - 单例模式【Singleton Pattern】
设计模式3 - 单例模式【Singleton Pattern】
38 0
|
设计模式 安全 Java
Java单例模式(Singleton Pattern)
Java单例模式(Singleton Pattern)
|
安全 Java
单例模式(Singleton Pattern)(三)
上篇文章我们讲述了,单例的推荐使用方式,以及反射对单例的破坏,但文末留了一个疑问,就是序列化如何破坏一个单例,那么本篇文章就来分析一下。
83 2
单例模式(Singleton Pattern)(三)
|
Java
单例模式(Singleton Pattern)(二)
上文我们了解常见的单例模式的创建方式,但还有没有其他的方式呢?在日常开发中推荐使用哪种呢?本文将带你深入了解其他的单例创建方式,以及单例模式的破坏。
164 1
单例模式(Singleton Pattern)(二)
|
设计模式 存储 缓存
详解Java设计模式之单例模式(Singleton Pattern)
详解Java设计模式之单例模式(Singleton Pattern)
241 0
详解Java设计模式之单例模式(Singleton Pattern)
|
安全 Java
创建型 - 单例模式(Singleton pattern)
创建型 - 单例模式(Singleton pattern)
创建型 - 单例模式(Singleton pattern)
|
设计模式 安全 Java
创建型模式 - 单例模式(Singleton Pattern)
创建型模式 - 单例模式(Singleton Pattern)
|
设计模式 安全 调度
【Singleton Pattern】设计模式之单例模式
【Singleton Pattern】设计模式之单例模式
166 0
【Singleton Pattern】设计模式之单例模式