首页 > 其他分享 >设计模式 - Singleton pattern 单例模式

设计模式 - Singleton pattern 单例模式

时间:2024-06-20 22:01:04浏览次数:14  
标签:Singleton getInstance pattern instance 实例 线程 单例 设计模式 public

文章目录

定义

在这里插入图片描述

单例模式是一种创建型设计模式,它用来保证一个类只有一个实例, 并且提供一个访问该实例的全局节点。其在很多场景中都有应用,比如数据库连接池、日志记录器、Spring中对象的创建等。

总的来说,单例模式在需要控制实例数量、确保全局唯一性的场景中被广泛应用。单例模式通过限制类的实例化对象为一个,可以确保全局唯一性的场景中被广泛应用,从而有助于控制资源访问、简化全局访问点、减少内存占用等,在很多情况下都可以提升程序的运行效率。

单例模式的实现构成

构成

一个私有的构造函数、一个私有的静态变量以及一个共有的静态函数。

其中,私有构造函数保证了其他线程不能通过new来创建对象实例,而共有的静态函数则是用来后续所有对此函数的调用都返回唯一的私有静态变量。

UML图

在这里插入图片描述

单例模式的六种实现

懒汉式-线程不安全

下面实现中,instance 被延迟实例化,这样的话,当没有使用到这个类的话,就会节约资源,不会实例化 LazySingletonsAreNotSafe

但是该实现是线程不安全的,因为在多线程环境下,可以有多个线程同时进入 getInstance 方法,并且这个时候 instance 还未实例化,那么它们就都可以进入到 if 逻辑中,执行实例化操作,从而导致线程不安全问题。

public class LazySingletonsAreNotSafe {
    private static LazySingletonsAreNotSafe instance;

    private LazySingletonsAreNotSafe() {}

    public static LazySingletonsAreNotSafe getInstance() {
        if (instance == null) {
            instance = new LazySingletonsAreNotSafe();
        }
        return instance;
    }
}

懒汉式-线程安全

那么,如何可以保证线程安全呢?

其实,上一个实现方式中,线程不安全就是因为 instance 的实例化被执行了很多次,所以我们只要对 getInstance 方法进行加锁,保证同一个时间点只有一个线程可以进入该方法进行实例化操作,那么就保证了线程安全问题。
实现代码如下:

public class LazySingletonsAreSafe {
    private static LazySingletonsAreSafe instance;

    private LazySingletonsAreSafe() {}

	// 关键点:synchronized进行了加锁操作,从而保证线程安全。
    public static synchronized LazySingletonsAreSafe getInstance() {
        if (instance == null) {
            instance = new LazySingletonsAreSafe();
        }
        return instance;
    }
}

饿汉式-线程安全

对于懒汉式方法,如果不加锁会导致线程安全问题,而加锁虽然会保证线程安全,但是也带来了一定程度上的性能损耗,因此可以采用饿汉式。
懒汉式线程安全问题的原因是 getInstance 方法可能被执行多次,从而导致被实例化多次。所以我们采用在类加载的时候,直接实例化 instance ,这样就会避免实例化多次的问题。

当然,因为我们一开始在类加载的时候对象就被实例化了,所以也不会有延迟实例化种可以节约资源的优点。

public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return instance;
    }
}

双重校验锁-线程安全

双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

public class DoubleCheckedLockingSingleton {
	// 注意:volatile 修饰
    private static volatile DoubleCheckedLockingSingleton instance;

    private DoubleCheckedLockingSingleton() {}

    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

问题1: 为什么两个if?

if (instance == null) {
     synchronized (DoubleCheckedLockingSingleton.class) {
         if (instance == null) {
             instance = new DoubleCheckedLockingSingleton();
         }
     }
 }

第一个if是因为:高并发场景下,还是可能有不止一个线程成功的在 instance 还未初始化的时候就进入这里了,所以他们都会走下面的逻辑,所以加了一把锁,用来保证线程安全问题。
而第二个if则是因为:等到第一个线程执行完实例化之后,它会释放锁,这样的话下一个线程就会来拿这把锁,然后进行新一轮的实例化。所以,在锁里添加了第二个if用来进行判断,避免实例化多次。

问题2: 为什么 instancevolatile 进行修饰?

private static volatile DoubleCheckedLockingSingleton instance;

这个是因为 volatile 有禁止指令重排的功能。上述代码中单例对象有的时候可能会发生空指针异常的问题。

对于instance = new DoubleCheckedLockingSingleton(); 它其实是分为三个步骤来执行的:

  1. JVM为对象分配内存
  2. 在内存中进行对象的初始化
  3. 将内存对应的地址复制给instance

假设,现在有两个线程进入到了getInstance方法,当T1线程执行实例化操作时,T2线程在进行判断。

因为instance = new DoubleCheckedLockingSingleton();操作不是原子的,所以编译器可能会进行指令的重排序,即:

  1. JVM为对象分配内存
  2. 将内存对应的地址复制给instance
  3. 在内存中进行对象的初始化

这样的话,当T1线程执行完第二步地址复制给instance的时候,T2线程去进行判断,那么instance == null则是为true,所以会直接跳到最下面 return instance。从而导致空指针问题。

volatile可以避免指令重排,所以只要用volatile修饰instance就可以避免这个问题了。
在这里插入图片描述

静态内部类实现

BillPughSingleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance 方法从而触发 SingletonHolder.INSTANCESingletonHolder才会被加载,进行初始化。

public class BillPughSingleton {
    private BillPughSingleton() {}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

枚举实现

枚举实例的创建是线程安全的,而且在任何情况下都是它一个单例。在别的几种单例中,反序列化时会重新创建对象,而枚举单例则不存在这种情况。

public enum EnumSingleton {
    INSTANCE;

    public void someMethod() {
    }
}

总结

  1. 饿汉式

    • 实现:在类加载时就完成了实例化。
    • 特点:线程安全,实现简单;但可能会造成资源浪费,因为即使不需要使用实例,也会在类加载时创建。
  2. 懒汉式

    • 实现:在第一次调用 getInstance() 方法时进行实例化。
    • 特点:延迟加载,节省资源;但需要在 getInstance() 方法上加锁才可以保证线程安全,会影响性能。
  3. 双重校验锁

    • 实现:在 getInstance() 方法中加入两次实例检查,第二次检查前加上锁,既保证了线程安全又提高了效率。
    • 特点:结合了懒汉式和饿汉式的优点,既实现了延迟加载,又优化了并发性能。
  4. 静态内部类

    • 实现:将单例实例放在静态内部类中,当外部类被加载时静态内部类并不会被加载,只有在首次调用 getInstance() 方法时才会加载。
    • 特点:既实现了延迟加载,又保证了线程安全,且不需显式同步。
  5. 枚举

    • 实现:利用枚举类型的特性来保证实例的唯一性。
    • 特点:线程安全,简洁易读,还能防止反序列化攻击。

其他设计模式文章:

标签:Singleton,getInstance,pattern,instance,实例,线程,单例,设计模式,public
From: https://blog.csdn.net/WLKQNYJY_SHT/article/details/139721071

相关文章

  • SVG <pattern> 标签的用法和应用场景
    通过使用<pattern>标签,可以在SVG图像内部定义可重复使用的任意图案。这些图案可以通过fill属性或stroke属性进行引用。使用场景例如我们要在<svg>中绘制大量的圆点点,可以通过重复使用<circle>标签来实现。<svgwidth="100"height="100"xmlns="http://www.......
  • SVG <pattern> 标签的用法和应用场景
    通过使用<pattern>标签,可以在SVG图像内部定义可重复使用的任意图案。这些图案可以通过fill属性或stroke属性进行引用。使用场景例如我们要在<svg>中绘制大量的圆点点,可以通过重复使用<circle>标签来实现。<svgwidth="100"height="100"xmlns="http://www.w3.org......
  • 速学设计模式 - 适配器模式
    废话写在前面吧(应届生或在校生看):当还是应届生的时候,找到好工作的机会是远大于后面的。所以现在毕业季,各位应届生,多去向上尝试。如果你已经在一家公司实习,你有更好的机会,就不要觉得不好意思,也不要被领导的饼唬到,到你手上的才是真实的。正文:适配器模式是一种结构性设计模式,它......
  • 23种设计模式之享元模式
    享元模式1、定义享元模式:运用共享技术有效的支持大量细粒度对象的复用2、享元模式结构Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(状态),同时也可以通过这些方法来设置外部数据(状态)Conc......
  • 多线程设计模式之Future模式
    在JDK中实现线程同步等待闭环(FutureTask/Future)中已经涉及到了Future模式,相对与多线程设计模式之WorkerThread模式有何异同呢?在多线程设计模式之WorkerThread模式中client和worker之间没有任何直接联系,即worker执行的结果client是不关心的;Future模式与之的差别就在于此线......
  • 赶紧收藏!2024 年最常见 20道设计模式面试题(七)
    上一篇地址:赶紧收藏!2024年最常见20道设计模式面试题(六)-CSDN博客十三、享元模式如何优化资源使用?享元模式(FlyweightPattern)是一种结构型设计模式,用于减少创建对象的数量,以减少内存占用和提高性能。这种模式通过共享多于一个对象共同使用的相同状态,来优化资源使用。享元模......
  • 设计模式之-组合模式
    组合模式是一种结构型设计模式,用于将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得客户端可以统一地处理单个对象和对象组合,无需区分它们的差异。组合模式主要解决的问题是当我们需要处理对象的层次结构,并希望能够以统一的方式对待单个对象和对象组合时。它使......
  • java设计模式-抽象工厂模式
    抽象工厂模式是一种创建一系列相关或相互依赖对象的设计模式。它通过定义一个抽象工厂接口,该接口声明了一组用于创建不同类型对象的方法,具体的工厂类实现这个接口并负责创建具体的对象。抽象工厂模式属于创建型模式的一种,可以帮助我们创建一族相关的对象。抽象工厂模式主要解决的......
  • 持续总结中!2024年面试必问 20 道设计模式面试题(六)
    上一篇地址:持续总结中!2024年面试必问20道设计模式面试题(五)-CSDN博客十一、迭代器模式(IteratorPattern)解决了什么问题?迭代器模式(IteratorPattern)解决了如何顺序访问一个聚合对象中的各个元素,而不暴露其内部的表示的问题。这种模式定义了一种方法来遍历集合,同时保持了对象......
  • 设计模式-利用状态机实现订单状态流转控制
    状态机是状态模式的一种应用,相当于上下文角色的一个升级版。在工作流和游戏中有大量使用。如各种工作流引擎,几乎是状态机的子集和实现,封装状态的变化规则。Spring也给我们提供了一个很好的解决方案。在spring中的组件名称就叫StateMachine。状态机简化状态控制的开发过程,让状态机......