单例模式(Singleton Pattern):确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式是创建型模式的一种,是创建型模式中最简单的设计模式 用于创建那些在软件系统中独一无二的对象。虽然单例模式很简单,但是它的使用频率还是很高的。
学习难度:★☆☆☆☆ | 使用频率:★★★★☆ |
---|
任务管理器相信大家都不陌生,大家可以用自己的电脑做个尝试,在Windows的任务栏的右键菜单中多次点击“任务管理器”,看能否打开多个任务管理器窗口。正常情况下,无论任务管理器启动多少次,Windows系统始终只会打开一个任务管理器窗口,也就是说,在一个Windows系统中,任务管理器存在唯一性。
在实际的开发中也经常遇到过类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功后,无法再创建一个同类型的其它对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,可以通过单例模式来实现,这就是单例模式的动机所在。
单例模式(Singleton Pattern):确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类也称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
从上图可以看出,单例类模式结构图中只包含一个单例角色。Singleton(单例):
A科技公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,即只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键。
A科技公司的研发部开发人员通过分析和权衡,决定使用单例模式来设计该负载均衡器。结构如下图:
在上边的结构图中,将负载均衡器LoadBalancer设计为单例类,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求,实现代码如下:
/// <summary>
/// 负载均衡器:单例类,真实环境可能非常复杂,这里只列出部分与模式相关的代码
/// </summary>
public class LoadBalancer
{
//私有静态成员变量,保存唯一实例
private static LoadBalancer instance = null;
//服务器集合
private List<string> serverList = null;
/// <summary>
/// 私有构造函数
/// </summary>
private LoadBalancer()
{
serverList = new List<string>();
}
/// <summary>
/// 公有静态成员方法,返回唯一实例
/// </summary>
/// <returns></returns>
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()
{
var random = new Random();
var i = random.Next(serverList.Count);
return serverList[i];
}
}
客户端测试代码:
class Program
{
static void Main(string[] args)
{
//创建4个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");
for (int i = 0; i < 10; i++)
{
var server = balancer1.GetServer();
Console.WriteLine("分发请求至服务器:" + server);
}
}
}
编译并运行程序,结果如下:
从运行结果可以看出,虽然我们创建了4个LoadBalancer对象,但他们是同一个对象。因此,通过使用单例模式可以确保LoadBalancer对象的唯一性。
研发部的开发人员使用单例模式实现了负载均衡器的设计,但是在实际使用中出现了一个非常严重的问题。当负载均衡器在启动过程中用户再次启动负载均衡器时,系统无任何异常,但是当客户端提交请求时出现请求分发失败。通过仔细分析发现原来系统中还是存在多个负载均衡器对象,导致分发时目标服务器不一致,从而产生冲突。
现在对负载均衡器的实现代码进行再次的分析。当第一次调用 GetLoadBalancer() 方法创建并启动负载均衡器时,instance 对象为 null,因此系统将执行代码 instance=new LoadBalancer()
,在此过程中,由于要对 LoadBalancer 进行大量初始化工作,需要一段时间来创建 LoadBalancer 对象。而此时如果再一次调用 GetLoadBalancer() 方法(通常发生在多线程环境中),由于 instance 尚未创建成功,此时仍然为null,判断条件“instance==null”仍然为true,代码 instance=new LoadBalancer()
将被再次执行,最终导致创建了多个 instance 对象,这违背了单例模式的初衷,也导致系统发生运行错误。
如何解决该问题?至少有两种解决方案,这就是接下来的饿汉式单例类和懒汉式单例类。
饿汉式单例类是实现起来最简单的单例类。定义一个静态变量,并在定义的时候就实例化单例类,这样在类加载的时候就已经创建了单例对象。
代码:
/// <summary>
/// 饿汉式单例
/// </summary>
public class EagerSingleton
{
//定义静态变量并实例化单例类
private static readonly EagerSingleton instance = new EagerSingleton();
//私有构造函数
private EagerSingleton()
{
}
//获取单例对象
public static EagerSingleton GetInstance()
{
return instance;
}
}
如果使用饿汉式单例来实现负载均衡器LoadBalancer的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。
除了饿汉式单例外,还有一种经典的懒汉式单例,也就是前边最开始提到的负载均衡器的实现方式。
懒汉式单例在第一次调用GetInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(lazy Load)技术,即需要的时候再加载实例。
为了避免多个线程同时调用GetInstance()方法,C#中可以使用 Lock
来进行线程锁定
/// <summary>
/// 懒汉式单例类
/// </summary>
public class LazySingleton
{
//私有静态成员变量,保存唯一实例
private static LazySingleton instance = null;
private static readonly object syncLocker = new object();
private LazySingleton() {}
//公有静态成员方法,返回唯一实例
public static LazySingleton GetInstance()
{
if (instance == null)
{
//锁定代码块
lock (syncLocker)
{
instance = new LazySingleton();
}
}
return instance;
}
}
问题似乎得到了解决,但事实并非如初。如果使用上边的代码来创建单例对象,仍然会出现单例对象不唯一的问题。原因如下:
假如某一瞬间线程A和线程B都在调用 GetInstance() 方法,此时 instance 对象为null,均能通过“instance==null”的判断。由于实现了加锁机制,线程A进入锁定的代码块中执行实例创建代码,那么此时线程B则处于排队等待状态,必须等线程A执行完毕后才可以进入lock代码块。但是当线程A执行完毕后,线程B并不知道实例已经创建,所以会继续进行新实例的创建,那么将会导致产生多个单例对象,违背了单例模式的设计思想。因此需要进一步改进,需要在锁定的代码块中再进行一次“instance==null”的判断,判断进入锁定代码块后是否有其它线程已经创建了单例类就可以了,这种方式称为双重检查锁定(Double-Check Locking)。代码如下:
/// <summary>
/// 懒汉式单例类
/// </summary>
public class LazySingleton
{
//私有静态成员变量,保存唯一实例
private static LazySingleton instance = null;
private static readonly object syncLocker = new object();
private LazySingleton() {}
/// <summary>
/// 公有静态成员方法,返回唯一实例
/// </summary>
/// <returns></returns>
public static LazySingleton GetInstance()
{
//第一重判读
if (instance == null)
{
//锁定代码快
lock (syncLocker)
{
//第二重判断
if (instance == null)
instance = new LazySingleton();
}
}
return instance;
}
}
饿汉式单例类:在类被加载时就将自己实例化。好处:
缺点:
懒汉式单例类:在类第一次使用时创建。好处:
缺点:
饿汉式单例类不能实现延迟加载,不管将来用不用,它始终占据着内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。有没有一种方法能够同时将这两种方式的缺点都克服呢?有!那就是静态内部类单例。
需要在单例类中增加一个静态(static)内部类。在该类内部中创建单例对象,再将该单例对象通过GetInstance()方法返回给外部使用,代码如下:
/// <summary>
/// 静态内部类单例,线程安全
/// </summary>
public class StaticSingleton
{
//私有构造函数,防止从外边实例化
private StaticSingleton(){}
//公有静态成员方法,返回唯一实例
public static StaticSingleton GetInstance()
{
return InnerClass.instance;
}
//内部类,第一次调用GetInstance()时加载InnerClass
class InnerClass
{
//在类被实例化或静态成员被调用的时候进行调用
//这里也就是当instance被调用的时候,会执行静态函数,初始化成员变量
static InnerClass(){}
internal static readonly StaticSingleton instance = new StaticSingleton();
}
}
instance并没有作为StaticSingleton的成员变量直接实例化,所以在类加载的时候不会实例化StaticSingleton。第一次调用GetInstance()方法时,将加载内部类InnerClass,该内部类定义了一个static类型的变量instance,这时首先会初始化这个成员变量,由.NET框架来保证线程安全性,确保该成员变量只能初始化一次。由于GetInstance()并没有被任何线程锁定,因此不会造成任何性能影响。
静态构造函数:
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率非常高,在很多软件和框架中都得以广泛的应用。
https://github.com/crazyliuxp/DesignPattern.Simples.CSharp