C# 设计模式:单例 (Singleton)
单例设计模式是一种创建型设计模式。
目的
单例设计模式的目的是确保一个类只有一个实例,并在应用程序的整个生命周期中提供一个全局访问点。访问单个实例是为了避免意外结果。
用法
可能有些功能需要你进行同步活动。例如,你想获取用户的投票,你不想出现重复计数,也不想遗漏任何投票计数。在这里,你可以使用带有 Vote 类的单例模式,以确保 Vote 类只创建一个实例,并用于计算每个用户的投票。
还有其他场景可以使用单例模式。例如,你可以在应用程序中使用单例模式实现日志记录功能,其中一个全局的日志记录器类实例用于记录应用程序中的所有信息。
单例类结构
一个类要实现单例模式,应具有以下结构
- 应具有私有或受保护的构造函数。没有公共和带参数的构造函数。
- 应具有一个静态属性(带有私有支持字段)来返回类的实例。也可以使用静态方法来返回实例。
- 至少有一个非静态的公共方法用于单例操作。
以下是 C# 中单例类的基本结构。
public class Singleton
{
private static Singleton _instance;
private Singleton()
{
}
public static Singleton Instance
{
get
{
if (_instance == null)
_instance = new Singleton();
return _instance;
}
}
public void DoSingletonOperation()
{
Console.WriteLine("singleton operation");
}
}
上述单例类使用静态属性返回类的实例。它有一个私有的无参数构造函数,这将限制使用 new 关键字创建对象。你必须使用 Instance 属性来获取它的对象。如果你想允许它在子类中被继承,可以将构造函数设置为 protected。
以下检查上述单例类是否每次都返回单个实例。
static void Main(string[] args)
{
Singleton s1 = Singleton.Instance;
Singleton s2 = Singleton.Instance;
Console.WriteLine(s1 == s2); // true
}
在上面的例子中,s1
和 s2
是同一个实例。然而,上述单例类不是线程安全的。它在多线程应用程序中可能会给出错误的结果。
实际应用中的单例类
让我们看看你可以实现单例设计模式的实际场景。
假设你在应用程序中获取用户的投票。多个用户从不同页面注册他们的投票。为此,你可以使用单例设计模式,如下所示。
public class VoteMachine
{
private VoteMachine _instance = null;
private int _totalVotes = 0;
private VoteMachine()
{
}
public static VoteMachine Instance
{
get
{
if (_instance == null) {
_instance = new VoteMachine();
}
}
return _instance;
}
}
public void RegisterVote()
{
_totalVotes += 1;
Console.WriteLine("Registered Vote #" + _totalVotes);
}
public int TotalVotes
{
get
{
return _totalVotes;
}
}
}
上述 VoteMachine
类是一个单例类,其构造函数是私有的,并且 Instance 属性每次都返回相同的实例。RegisterVote()
方法将投票计数增加 1。TotalVotes
属性返回已注册的总投票数。
让我们在控制台应用程序中测试上述 VoteMachine
类,如下所示。
internal class Program
{
static void Main(string[] args)
{
VoteMachine vm1 = VoteMachine.Instance;
VoteMachine vm2 = VoteMachine.Instance;
VoteMachine vm3 = VoteMachine.Instance;
vm1.RegisterVote();
vm2.RegisterVote();
vm3.RegisterVote();
Console.WriteLine(vm1.TotalVotes);
}
}
Registered Vote #1 Registered Vote #2 Registered Vote #3 3
VoteMachine
单例类将在同步调用中完美工作,每个用户将逐个注册他们的投票。
等一下,如果每个用户都将逐个注册他们的投票,那么我们为什么需要单例类呢?
在实际场景中,可能有多用户在不知不觉中异步注册他们的投票。让我们看看 VoteMachine
类在并行投票(多线程环境)中的表现。
以下演示了使用 Parallel 类在多线程环境中测试 VoteMachine
类。
internal class Program
{
static void Main(string[] args)
{
var numbers = Enumerable.Range(0, 10);
Parallel.ForEach(numbers, i =>
{
var vm = VoteMachine.Instance;
vm.RegisterVote();
});
Console.WriteLine(VoteMachine.Instance.TotalVotes);
}
}
Registered Vote #1 Registered Vote #1 Registered Vote #1 Registered Vote #1 Registered Vote #1 Registered Vote #1 Registered Vote #1 Registered Vote #1 Registered Vote #1 Registered Vote #2 2
上述代码执行 10 次并行调用 RegisterVote()
函数。输出将取决于你的本地机器。每次运行输出可能都不同。输出在多线程调用中返回错误的结果。
让我们看看如何创建一个线程安全的单例类。
线程安全的单例类
在创建单例类的对象之前使用线程锁,使其成为线程安全的。
public class VoteMachine
{
private static VoteMachine _instance = null;
private int _totalVotes = 0;
private static readonly object lockObj = new object();
private VoteMachine()
{
}
public static VoteMachine Instance
{
get
{
lock (lockObj)
{
if (_instance == null)
{
_instance = new VoteMachine();
}
}
return _instance;
}
}
public void RegisterVote()
{
_totalVotes += 1;
Console.WriteLine("Registered Vote #" + _totalVotes);
}
public int TotalVotes
{
get
{
return _totalVotes;
}
}
}
在上述 VoteMachine
类中,我们锁定创建 VoteMachine
类实例的代码。这意味着只有一个线程可以进入锁并执行代码并创建实例。请注意,每次请求实例时都会获取锁,因此性能会下降。
现在,让我们在多线程场景中测试上述 VoteMachine
类,如下所示。
public class Program
{
public static void Main(string[] args)
{
var numbers = Enumerable.Range(0, 10);
Parallel.ForEach(numbers, i =>
{
var vm = VoteMachine.Instance;
vm.RegisterVote();
});
Console.WriteLine(VoteMachine.Instance.TotalVotes);
}
}
Registered Vote #3 Registered Vote #9 Registered Vote #5 Registered Vote #7 Registered Vote #8 Registered Vote #6 Registered Vote #2 Registered Vote #3 Registered Vote #10 Registered Vote #4 10
输出可能有所不同,但它将显示正确的总票数。尝试多次运行以确保总票数正确。
为了提高性能,我们可以在锁定前后双重检查 _instance == null
,如下所示。
public class VoteMachine
{
private static VoteMachine _instance = null;
private int _totalVotes = 0;
private static readonly object lockObj = new object();
private VoteMachine()
{
}
public static VoteMachine Instance
{
get
{
if (_instance == null)
{
lock (lockObj)
{
if (_instance == null)
{
_instance = new VoteMachine();
}
}
}
return _instance;
}
}
public void RegisterVote()
{
_totalVotes += 1;
Console.WriteLine("Registered Vote #" + _totalVotes);
}
public int TotalVotes
{
get
{
return _totalVotes;
}
}
}
上述代码在没有内存屏障的情况下,与 ECMA CLI 规范存在一些问题。
使用静态构造函数创建单例类
你可以通过使用静态构造函数来创建单例类。当访问类的任何静态成员时,静态构造函数在每个应用程序域中只运行一次。
public class VoteMachine
{
private static readonly VoteMachine _instance = new VoteMachine();
private int _totalVotes = 0;
static VoteMachine()
{
}
private VoteMachine()
{
}
public static VoteMachine Instance
{
get
{
return _instance;
}
}
public void RegisterVote()
{
_totalVotes += 1;
Console.WriteLine("Registered Vote #" + _totalVotes);
}
public int TotalVotes
{
get
{
return _totalVotes;
}
}
}
上述 VoteMachine
是一个带有静态构造函数的单例类。私有构造函数阻止使用 new 关键字创建实例。
上述类一旦我们访问任何静态属性或方法,就会立即创建一个实例。如果出于某种原因有多个静态属性或方法,那么即使我们不打算使用它,也会立即创建一个实例。我们需要惰性实例化,它只在必要时才创建实例。
带有惰性实例化的单例类
如果你使用 .NET 4 或更高版本,请使用 Lazy<T> 仅在需要时创建实例。
public sealed class VoteMachine
{
private static readonly Lazy<VoteMachine> _instance = new Lazy<VoteMachine>(() => new VoteMachine());
private int _totalVotes = 0;
private VoteMachine()
{
}
public static VoteMachine Instance
{
get
{
return _instance.Value;
}
}
public void RegisterVote()
{
_totalVotes += 1;
Console.WriteLine("Registered Vote #" + _totalVotes);
}
public int TotalVotes
{
get
{
return _totalVotes;
}
}
}
上面的代码隐式地使用 LazyThreadSafetyMode.ExecutionAndPublication
作为 Lazy<VoteMachine>
的线程安全模式。Lazy<T>
使惰性实例化变得简单且性能良好。它还允许你通过 IsValueCreated
属性检查实例是否已创建。