设计模式 | 单例模式

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
传统型负载均衡 CLB,每月750个小时 15LCU
简介: 单例模式:确保一个类只有一个实例对象,并提供一个全局访问点来访问这个唯一的实例。Singleton Pattern:Ensure a class has only one instance, and provide a global point of access to it.

1 | 单例模式概述

对于一个系统中的某些类而言,只有一个实例很重要。例如:

  • 一个系统只能有一个窗口管理器或文件系统
  • 一个系统只能有一个日历工具或ID序号生成器等

image.png

以上这些类似的例子,在系统中如果这些窗口不唯一化,那么系统就会出现很多重复的窗口(内容完全一致),这些窗口就是重复的对象,会对系统内存资源造成严重的浪费。因此,有些时候在系统中某个对象的唯一性(即一个类只能有一个实例)非常重要。

思考:如何创建一个唯一的实例,并且容易被访问呢?

如果定义一个全局统一的变量可以确保对象随时都可以被访问,但是不能防止创建多个对象,这对这种情况最好的解决办法是让类自身负责创建和保存它的唯一实例,并保证不能创建其他的实例,同时对外提供一个访问该实例的方法,这就是单例模式的动机。

1.1 单例模式的定义

单例模式:确保一个类只有一个实例对象,并提供一个全局访问点来访问这个唯一的实例。

Singleton Pattern:Ensure a class has only one instance, and provide a global point of access to it.

1.2 单例模式的 3 要素

单例模式是一种结构最简单的设计模式,是一种对象创建型模式,有以下 3 个要点:

  • 该类有且仅有一个实例对象;
  • 该类必须自行创建这个实例对象;
  • 该类必须自行向整个系统提供这个实例对象;

2 | 单例模式的结构与实现

2.1 单例模式的结构

单例模式是最简单的设计模式,本身只包含一个类,即单例类。

2.2 单例模式的实现

目的:保证一个类有且仅有一个实例(避免对象资源重复创建,造成资源浪费),并提供一个访问它的全局访问点。

实现单例模式的注意事项:

  • 单例类构造函数的可访问性 private;
  • 提供一个类型为自身的静态私有只读成员变量;
  • 提供一个公有的静态工厂方法;

2.2.1 单例模式一:饿汉式单例类

public class EagerSingleton
{
    // 私有静态只读实例化对象
    private static readonly EagerSingleton _Instance = new EagerSingleton();
    // 设置私有静态构造函数
    private EagerSingleton() { }
    // 公开静态访问该类实例对象方法
    public static EagerSingleton GetInstance() => _Instance;
    //多线程环境下测试
    public string Say() => "SayHi,EagerSingleton!";
}

上面的代码中,当类别加载的时候,静态变量 _Instance 会被初始化,随后类的私有构造函数会被调用,单例类的唯一实例将被创建。

说明:类对象中加载执行的优先级,静态变量 > 静态构造函数/方法 > 构造函数/方法 > 静态函数/方法 > 实例变量或属性 > 实例方法。

2.2.2 单例模式二:懒汉式单例类与双重检查锁定(if+lock);

public class LazySingleton
{
    // 私有静态只读object对象
    private static readonly object _ObjLock = new object();
    // 私有静态只读实例化对象,volatile 促进线程安全,保证线程有序执行 
    private static volatile LazySingleton _Instance = null; 
    // 设置私有静态构造函数
    private LazySingleton() { }
    // 公开静态访问该类实例对象方法
    public static LazySingleton GetInstance()
    {
        // 第一重判断,先判断实例是否存在,不存在再加锁处理
        if (_Instance == null)
        {
            // 加锁的程序在某一时刻只允许一个线程访问
            lock (_ObjLock) 
            {
                // 第二重判断,防止实例被重复创建
                if (_Instance == null)
                {
                    _Instance = new LazySingleton();
                }
            }
        }
        return _Instance;
    }
 
    //多线程环境下测试
    public string Say() => "SayHi,LazySingleton!";
}

懒汉式单例类在第一次被引用时将自己实例化,但加载时不会将自己实例化。

客户端调用如下所示:

image.png

懒汉式单例类添加双重 if 检查和 lock 关键字的原因何在?

我们来思考一个场景,如果在高并发、多线程环境下实现懒汉式单例类,在某一时刻可能会有多个线程需要使用单例对象,因此会有多个线程同时调用 GetInstance() 方法,当这一刻发生时可能会创建多个实例对象,此时就违背了单例模式的目标。为了防止这种现象的发生,需要使用到 C# 中的 lock(混合锁) 关键字,该关键字锁定的代码片段称为临界区,可以确保一个线程位于代码的临界区。如果其他线程尝试进入锁定的代码,则将一直等待,直到该对象被释放为止。

那么既然添加了 lock 锁定,又再次添加双重 if 非空检查判断,这又是搞个啥玩意呢?
我们为什么需要这么做,这么所到底是为了啥?

原因很简单,双重检查(Double-Check Locking)的机制是为了更好的控制单例对象的创建,当实例不存在且同时有两个线程调用 GetInstance() 方法时,第一重检查机制都对2个线程生效,此时进入 lock 机制,一个线程进入 lock 代码块,另外一个线程处于排队等待状态,并且必须等待进入的线程执行完毕后才能进入 lock 代码块,假如此时不进行第二重检查机制,第 2 个进入的线程并不知道实例已经被第 1 个线程创建了,那么它将继续创建一个对象,此时还是会产生多个实例对象,结果还是违背了单例模式的设计思想,因此需要双重检查机制。

2.2.3 饿汉式单例类与懒汉式单例类的比较

  • 饿汉式单例类 在类被加载时就将自己实例化,有点无需考虑多个线程同时访问的问题,并且可以确保实例的唯一性。饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多个线程同时访问的问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
  • 懒汉式单例类 在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然会涉及资源初始化。而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的几率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

3 | 单例模式的应用实例

3.1 实例说明

某软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高了系统的整体处理能力,缩短了响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。

如何确保 负载均衡器的唯一性 是该软件成功的关键,试使用单例模式设计服务器负载均衡器。

LoadBalancer.cs 代码设计如下:

/// <summary>
/// 负载均衡器
/// </summary>
class LoadBalancer
{
    //私有静态成员变量,存储唯一实例
    private static LoadBalancer instance = null;
    //服务器集合
    private static ArrayList serverList = null;
 
    //私有构造函数
    private LoadBalancer()
    {
        serverList = new ArrayList();
    }
 
    //公有静态成员方法,返回唯一实例
    public static LoadBalancer GetLoadBalancer()
    {
        if (instance == null)
        {
            instance = new LoadBalancer();
        }
        return instance;
    }
 
    //增加服务器
    public void AddServer(string server)
    {
        serverList.Add(server);
    }
 
    //删除服务器
    public void RemoveServer(string server)
    {
        serverList.Remove(server);
    }
 
    //使用Random类随机获取服务器
    public string GetServer()
    {
        Random random = new Random();
        int i = random.Next(serverList.Count); //随机选一台服务器
        return serverList[i].ToString();
    }
}

客户端调用,此处使用控制台演示,调用 LoadBalancer.cs 代码演示如下:

image.png

备注:完成代码实例请查看, DesignPattern: DesignPattern GoF 23+1 | 设计模式 23+1 - Gitee.com

4 | 单例模式的优缺点与适用环境

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。

4.1 单例模式的主要优点

  • (1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以可以严格控制客户怎样访问它以及何时访问它。
  • (2)由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,使用单例模式无疑可以提高系统的性能
  • (3)单例模式允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题
注:自行提供指定数目实例对象的类可称为多例类。

4.2 单例模式的主要缺点

  • (1)由于单例模式中没有抽象层,因此,单例类的扩展有很大的困难
  • (2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
  • (3)现在很多面向对象语言(如:C#、Java)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,在下次利用时又将重新实例化,这将导致共享的单例对象状态丢失

4.3 单例模式的适用环境

  • (1)系统只需要一个实例对象,例如系统要求提供一个唯一的序列号生成器或资源管理器,或者因为资源消耗太大而只允许创建一个对象。
  • (2)客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
目录
相关文章
|
7月前
|
设计模式 缓存 安全
【设计模式】【创建型模式】单例模式(Singleton)
一、入门 什么是单例模式? 单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。它常用于需要全局唯一对象的场景,如配置管理、连接池等。 为什么要单例模式? 节省资源 场景:某些对象创
247 15
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
113 2
|
9月前
|
设计模式 安全 Java
设计模式:单例模式
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。它通过私有化构造函数、自行创建实例和静态方法(如`getInstance()`)实现。适用于数据库连接池、日志管理器等需要全局唯一对象的场景。常见的实现方式包括饿汉式、懒汉式、双重检查锁、静态内部类和枚举。线程安全问题可通过`synchronized`或双重检查锁解决,同时需防止反射和序列化破坏单例。优点是避免资源浪费,缺点是可能增加代码耦合度和测试难度。实际开发中应优先选择枚举或静态内部类,避免滥用单例,并结合依赖注入框架优化使用。
|
8月前
|
设计模式 存储 安全
设计模式-单例模式练习
单例模式是Java设计模式中的重要概念,确保一个类只有一个实例并提供全局访问点。本文详解单例模式的核心思想、实现方式及线程安全问题,包括基础实现(双重检查锁)、懒汉式与饿汉式对比,以及枚举实现的优势。通过代码示例和类图,深入探讨不同场景下的单例应用,如线程安全、防止反射攻击和序列化破坏等,展示枚举实现的简洁与可靠性。
141 0
|
10月前
|
设计模式 存储 安全
设计模式2:单例模式
单例模式是一种创建型模式,确保一个类只有一个实例,并提供全局访问点。分为懒汉式和饿汉式: - **懒汉式**:延迟加载,首次调用时创建实例,线程安全通过双重检查锁(double check locking)实现,使用`volatile`防止指令重排序。 - **饿汉式**:类加载时即创建实例,线程安全但可能浪费内存。 示例代码展示了如何使用Java实现这两种模式。
232 4
|
12月前
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
483 13
|
12月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
163 2
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
199 4
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
132 1

热门文章

最新文章