1 | 单例模式概述
对于一个系统中的某些类而言,只有一个实例很重要。例如:
- 一个系统只能有一个窗口管理器或文件系统;
- 一个系统只能有一个日历工具或ID序号生成器等;
以上这些类似的例子,在系统中如果这些窗口不唯一化,那么系统就会出现很多重复的窗口(内容完全一致),这些窗口就是重复的对象,会对系统内存资源造成严重的浪费。因此,有些时候在系统中某个对象的唯一性(即一个类只能有一个实例)非常重要。
思考:如何创建一个唯一的实例,并且容易被访问呢?
如果定义一个全局统一的变量可以确保对象随时都可以被访问,但是不能防止创建多个对象,这对这种情况最好的解决办法是让类自身负责创建和保存它的唯一实例,并保证不能创建其他的实例,同时对外提供一个访问该实例的方法,这就是单例模式的动机。
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!";
}
懒汉式单例类在第一次被引用时将自己实例化,但加载时不会将自己实例化。
客户端调用如下所示:
懒汉式单例类添加双重 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
代码演示如下:
备注:完成代码实例请查看, 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)客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。