思考并回答以下问题:
- 饿汉式单例与懒汉式单例有什么区别?
本章导学
单例模式是结构最简单的设计模式,在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以确保系统中的一个类只有一个实例,而且该实例易被外界访问,从而方便对实例个数进行控制,节约系统资源。
本章将学习如何使用单例模式来确保系统中某个类的实例对象的唯一性,学习单例模式的实现方式以及如何在实际项目开发中合理地使用单例模式。
本章知识点
- 单例模式的定义
- 单例模式的结构
- 单例模式的实现
- 单例模式的应用
- 单例模式的优缺点
- 单例模式的适用环境
- 饿汉式单例和懒汉式单例
单例模式概述
对于一个软件系统中的某些类而言,只有一个实例很重要。例如,一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器等。在Windows操作系统中只能打开一个任务管理器窗口,如图1所示。如果不使用机制对窗口对象进行唯一化,势必会弹出多个窗口。如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此,有时确保系统中某个对象的唯一性(即一个类只能有一个实例)非常重要。
图1 Windows任务管理器
如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个统一的全局变量可以确保对象随时都可以被访问,但不能防止创建多个对象。一个更好的解决办法是让类自身负责创建和保存它的唯一实例,并保证不能创建其他实例,它还提供了一个访问该实例的方法,这就是单例模式的动机。
单例模式的定义如下:1
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
单例模式是一种对象创建型模式。单例模式有3个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
单例模式的结构与实现
单例模式的结构
单例模式是结构最简单的设计模式,它只包含一个类,即单例类。单例模式的结构如图2所示。
图2 单例模式结构图
由图2可知,单例模式只包含一个单例角色——Singleton(单例),在单例类的内部创建它的唯一实例,并通过静态方法GetInstance()让客户端可以使用它的唯一实例。为了防止在外部对单例类实例化,将其构造函数的可见性设为private,并在单例类内部定义了一个Singleton类型的静态对象,作为供外部共享访问的唯一实例。
单例模式的实现
单例模式的目的是保证一个类有且仅有一个实例,并提供一个访问它的全局访问点。单例模式包含的角色只有一个,就是单例类——Singleton,单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,单例类中还包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
通常,单例模式的实现代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Singleton
{
private static Singleton instance = null; // 静态私有成员变量
// 私有构造函数
private Singleton()
{
}
// 静态公有工厂方法,返回唯一实例
public static Singleton GetInstance()
{
if(instance == null)
instance = new Singleton();
return instance;
}
}
为了测试单例类所创建对象的唯一性,可以编写以下客户端测试代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18using System;
class Program
{
static void Main(string[] args)
{
Singleton s1 = Singleton.GetInstance();
Singleton s2 = Singleton.GetInstance();
// 判断两个对象是否相同
if (s1 == s2)
{
Console.WriteLine("两个对象是相同实例。");
}
Console.Read();
}
}
编译代码并运行,输出结果为:1
两个对象是相同实例。
说明两次调用Getinstance()所获取的对象是同一实例对象,且无法在外部对Singletor进行实例化,因此能够确保系统中只有唯一的一个Singleton对象。
在单例模式的实现过程中,用户需要注意以下3点:
- (1)单例类构造函数的可见性为private。
- (2)提供一个类型为自身的静态私有成员变量。
- (3)提供一个公有的静态工厂方法。
单例模式的应用实例
下面通过一个应用实例来进一步学习和理解单例模式。
1.实例说明
某软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高了系元的整体处理能力,缩短了响应时间。由于集群中的服务器需要动态刑减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键,试使用单例模式设计服务器负载均衡器。
2.实例类图
通过分析,本实例的结构如图3所示。
图3 服务器负载均衡结构图
在图3中,将负载均衡器LoadBalancer设计为单例角色,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求。
3.实例代码
(1)LoadBalancer:负载均衡器类,充当单例角色。在真实环境下该类非常复杂,包括大量初始化的工作和业务方法,考虑到代码的可读性和易理解性,在此只列出部分与模式相关的核心代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49using System;
using System.Collections;
namespace SingletonSample
{
class LoadBalancer
{
// 私有静态成员变量,存储唯一实例
private static LoadBalancer instance = null;
// 服务器集合
private 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();
}
}
}
(2)Program:客户端测试类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37using System;
namespace SingletonSample
{
class Program
{
static void Main(string[] args)
{
// 创建四个LoadBalancer对象
LoadBalancer balancer1,balancer2,balancer3,balancer4;
balancer1 = LoadBalancer.GetLoadBalancer();
balancer2 = LoadBalancer.GetLoadBalancer();
balancer3 = LoadBalancer.GetLoadBalancer();
balancer4 = LoadBalancer.GetLoadBalancer();
// 判断服务器负载均衡器是否相同
if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4)
{
Console.WriteLine("服务器负载均衡器具有唯一性!");
}
// 增加服务器
balancer1.AddServer("Server 1");
balancer1.AddServer("Server 2");
balancer1.AddServer("Server 3");
balancer1.AddServer("Server 4");
// 模拟客户端请求的分发,如果输出结果全为同一个server,可以将i适当放大,例如改为"i < 100"
for (int i = 0; i < 10; i++)
{
string server = balancer1.GetServer();
Console.WriteLine("分发请求至服务器: " + server);
}
Console.Read();
}
}
}
4.结果及分析
编译并运行程序,输出结果如下:1
2
3
4
5
6
7
8
9
10
11服务器负载均衡器具有唯一性!
分发请求至服务器: Server 1
分发请求至服务器: Server 1
分发请求至服务器: Server 1
分发请求至服务器: Server 1
分发请求至服务器: Server 2
分发请求至服务器: Server 2
分发请求至服务器: Server 2
分发请求至服务器: Server 2
分发请求至服务器: Server 3
分发请求至服务器: Server 3
虽然创建了4个LoadBalancer对象,但是它们实际上是同一个对象,因此,通过使用单例模式可以确保LoadBalancer对象的唯一性。
饿汉式单例与懒汉式单例
1.饿汉式单例类
饿汉式单例类(Eager Singleton)是实现起来最简单的单例类,饿汉式单例类结构如图4所示。
图4 饿汉式单例类图
从图4中可以看出,由于在定义静态变量的时候实例化了单例类,因此在类加载时单例对象就已创建,代码如下:1
2
3
4
5
6
7
8
9
10
11class EagerSingleton
{
private static EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton GetInstance()
{
return instance;
}
}
当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。
2.懒汉式单例类与双重检查锁定
与饿汉式单例类相同的是,懒汉式单例类(Lazy Singleton)的构造函数也是私有的。与饿汉式单例类不同的是,懒汉式单例类在第一次被引用时将自己实例化,在懒汉式单例类被加载时不会将自己实例化。懒汉式单例类结构如图5所示。
图5 懒汉式单例类图
从图5中可以看出,在懒汉式单例类中,不是在定义静态变量时实例化单例类,而是在第一次调用静态工厂方法时实例化单例类。前面应用实例中的负载均衡器类LoadBalancer就是采用懒汉式单例来实现的。
但是懒汉式单例存在一个很严重的问题:如果在高并发、多线程环境下实现懒汉式单例类,在某一时刻可能会有多个线程需要使用单例对象,即会有多个线程同时调用GetInstance()方法,可能会造成创建多个实例对象,这将违背单例模式的设计意图。为了防止生成多个单例对象,需要使用C#语言中的lock关键字,lock关键字锁定的代码片段称为临界区,可以确保当一个线程位于代码的临界区时,另一个线程不能进入临界区。如果其他线程试图进入锁定的代码,则将一直等待,直到该对象被释放为止。修改之后的懒汉式单例类代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class LazySingleton
{
private static LazySingleton instance = null;
// 程序运行时创建一个静态只读的辅助对象
private static readonly object syncRoot = new object();
private LazySingleton(){}
public static LazySingleton GetInstance()
{
// 第一重判断,先判断实例是否存在,不存在再加锁处理
if (instance == null)
{
// 加锁的程序在某一时刻只允许一个线程访问
lock(syncRoot)
{
// 第二重判断
if (instance == null)
{
instance = new LazySingleton(); // 创建单例实例
}
}
}
return instance;
}
}
在上面给出的懒汉式单例类实现代码中,对静态工厂方法GetInstance()中创建单例对象的代码进行了加锁、由于在调用时无法确定该单例对象是否已创建,因此需要使用辅助对象syncRoot来进行代码锁定。为了不影响程序的性能,此处只锁定创建单例对象的代码,并未锁定整个方法。如果实例存在则直接返回,如果实例未创建则加锁后再创建。
为了更好地对单例对象的创建进行控制,此处使用了一种被称为双重检查锁定(Double-Check Locking)的双重判断机制。在双重检查锁定中,当实例不存在且同时有两个线程调用GetInstance()方法时,它们都可以通过第一重“instance==null”判断,并且由于lock锁定机制,只有一个线程进入lock中执行创建代码,另一个线程处于排队等待状态,必须等待第一个线程执行完毕才可以进入lock锁定的代码,如果此时不进行第二重“instance-=null”判断,第二个线程并不知道实例已经创建,将继续创建新的实例,还是会产生多个单例对象,违背了单例模式的设计思想,因此需要进行双重检查。
3.饿汉式单例类与懒汉式单例类比较
饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多个线程同时访问的问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然会涉及大量时间,这意味着出现多线程同时首次引用此类的概率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。
单例模式的优缺点与适用环境
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
单例模式的优点
单例模式的主要优点如下:
- (1)单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以可以严格控制客户怎样访问它以及何时访问它。
- (2)由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,使用单例模式无疑可以提高系统的性能。
- (3)单例模式允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题(自行提供指定数目实例对象的类可称为多例类)。
单例模式的缺点
单例模式的主要缺点如下:
- (1)由于单例模式中没有抽象层,因此,单例类的扩展有很大的困难。
- (2)单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起
- (3)现在很多面向对象语言(如C#、Java)的运行环境都提供了自动垃圾回收技术,因此,实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,在下次利用时又将重新实例化,这将导致共享的单例对象状态丢失。
单例模式的适用环境
在以下情况下可以考虑使用单例模式:
- (1)系统只需要一个实例对象,例如系统要求提供一个唯一的序列号生成器或资源管理器,或者因为资源消耗太大而只允许创建一个对象。
- (2)客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
本章小结
(1)单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供了全局访问的方法。单例模式是一种对象创建型模式。
(2)单例模式只包含一个单例角色。单例类的构造函数为私有,它提供一个自身类型的静态私有成员变量和一个公有的静态工厂方法。
(3)单例模式的主要优点在于提供了对唯一实例的受控访问并可以节约系统资源;其主要缺点在于因为缺少抽象层而难以扩展,且单例类职责过重,将太多的功能耦合在一起
(4)单例模式适用的环境:系统只需要一个实例对象;客户调用类的单个实例只允许使用一个公共访问点。
(5)饿汉式单例在类加载的时候创建唯一实例,懒汉式单例在第一次调用静态工厂方法时创建唯一实例。
(6)在懒汉式单例类中,为了确保线程安全,避免创建多个单例对象,需要使用双重检查锁定机制来对单例对象的创建进行控制。