模式简介
单例模式是一种简单的设计模式,当我们需要在多个不同的地方访问A类中的方法时,往往会想到使用静态方法访问的方式,也就是把A类中可访问的方法都设置为公开的静态方法,这种方式虽然也可以达到类似单例的效果,但是二者存在一定的区别。
如果我们需要访问A类的实例方法时,就需要通过创建A类的实例来进行访问,如果我们需要在多个不同的地方访问A类中的实例方法,那就需要创建多个A类的实例,这会带来不必要的资源开销,因为如果A类不是作为一个会有不同特征的实体类时,就不需要反复去new实例。如果A类只是作为一个功能集合的工具类,就只需要一个A类的实例就可以了,这就可以采用单例模式,我们将A类的构造方法进行私有化,A类的实例创建仅由A类来决定,A类向外提供一个获取该实例的方法以便调用者可以访问A类的实例方法,这样就可以避免调用者new多个A类实例了,进而避免资源的浪费。
常见的应用场景:资源共享、配置管理器、日志记录器、线程池、缓存管理器、GUI应用程序中的窗口管理器、数据库连接池、打印机池等。
特征 | 单例模式 | 静态方法 |
---|---|---|
创建方式 | 单例模式通过类的实例化来创建对象,通常具有私有构造函数,提供一个静态方法或属性来获取唯一的实例。 | 静态方法属于类而不是实例,通过类名直接调用,不需要创建对象。 |
实例控制 | 单例模式通过实例化控制确保在整个应用程序中只有一个实例存在。 | 静态方法不涉及实例,因此没有实例控制。 |
灵活性 | 单例模式相对更灵活,可以实现接口、继承,并支持多态性。 | 静态方法缺乏灵活性,不能实现接口,不支持多态性。 |
可测试性 | 单例模式相对更易于测试,可以通过依赖注入或模拟实例来进行单元测试。 | 静态方法测试较为困难,因为它们通常难以被模拟或替换。 |
依赖注入 | 单例模式支持依赖注入,可以通过构造函数或其他方法注入依赖关系。 | 静态方法通常不支持依赖注入,因为它们无法通过实例化来传递依赖关系。 |
多态性 | 单例模式支持多态性,可以通过接口引入多态性,使得代码更具可扩展性。 | 静态方法不支持多态性,因为它们与类而不是实例关联。 |
延迟加载 | 单例模式支持延迟加载,即只有在需要的时候才创建实例。 | 静态方法在程序启动时就加载并执行,不支持延迟加载。 |
全局状态管理 | 单例模式更适合用于全局状态管理,因为它可以在需要时创建一个实例,并且实例的生命周期由应用程序控制。 | 静态方法无法维护实例级别的状态,不适合全局状态管理。 |
实例级别状态 | 单例模式可以维护实例级别的状态,每次访问都是同一个实例。 | 静态方法通常无法维护实例级别的状态。 |
模式结构
-
单例类(Singleton): 定义一个私有的静态变量用于保存唯一的实例,提供一个公共的静态方法用于获取该实例。通常实现为一个私有构造函数,确保只能通过特定的方式来创建实例。
-
客户端(Client): 使用单例的类或模块,通过静态方法或属性获取单例实例,并调用其方法。
工作原理
-
私有化构造函数: 单例类通常将其构造函数私有化,以防止通过普通的构造函数直接实例化对象。
-
私有静态变量: 单例类内部通常包含一个私有的静态变量,用于存储唯一的实例。
-
静态方法或属性: 单例类提供一个公共的静态方法或属性,用于获取该类的唯一实例。在这个方法中,通常会检查实例是否已经存在,如果存在则返回已有的实例,否则创建一个新的实例。
-
懒加载或饿汉式: 单例模式可以采用懒加载(延迟加载)或饿汉式(在类加载时立即创建实例)的方式来创建实例,取决于具体的需求。
-
线程安全性: 如果在多线程环境中使用单例模式,需要考虑线程安全性。常见的做法是使用双重检查锁定(Double-Checked Locking)或者使用静态初始化块等方式来确保在并发情况下仍然能够正确创建单例。
代码示例(C#)
提示:可在本栏目的资源篇“设计模式代码示例合集”下载所有完整代码资源。
单例模式:SingleTonPattern.cs
namespace SingleTonPattern;
// 单例模式
class SingleTonPatternManager
{
// 测试方法
public void Test()
{
// 模拟多线程访问单例
for (int i = 0; i < 30; i++)
{
Thread.Sleep(10);
new Thread(() =>
{
SingleTon1 singleTon1 = SingleTon1.GetInstance();
Console.WriteLine("singleTon1:" + singleTon1.GetHashCode());
}).Start();
new Thread(() =>
{
SingleTon2 singleTon2 = SingleTon2.GetInstance();
Console.WriteLine("singleTon2:" + singleTon2.GetHashCode());
}).Start();
new Thread(() =>
{
SingleTon3 singleTon3 = SingleTon3.GetInstance();
Console.WriteLine("singleTon3:" + singleTon3.GetHashCode());
}).Start();
}
}
// 单例模式:双重判空 + 锁 + 懒汉式加载
class SingleTon1
{
private static SingleTon1 instance;
private static readonly object k = new object();
private SingleTon1() { }
public static SingleTon1 GetInstance()
{
if (instance == null)
{
lock (k)
{
if (instance == null) instance = new SingleTon1();
}
}
return instance;
}
}
// 单例模式:内部类 + 懒汉式加载
class SingleTon2
{
private SingleTon2() { }
private class Handler
{
public static readonly SingleTon2 instance = new SingleTon2();
}
public static SingleTon2 GetInstance()
{
return Handler.instance;
}
}
// 单例模式:饿汉式加载
class SingleTon3
{
private readonly static SingleTon3 instance = new SingleTon3();
private SingleTon3() { }
public static SingleTon3 GetInstance()
{
return instance;
}
}
}
测试代码:Program.cs
// ************* 1.单例模式测试 **************
// using SingleTonPattern;
// SingleTonPatternManager singleTonPatternManager = new SingleTonPatternManager();
// singleTonPatternManager.Test();
代码解说
单例模式有很多种,这里我们展示了其中比较常用的三种。
SingleTon1所示的单例模式通过一个锁k来限制对instance的访问,之所以这里在获取锁k的前后分别进行了一次instance为空判断,是因为可能存在一个线程A正在执行new实例的代码,但是这时另一个线程B已经通过了外层的空判断,如果内层不加入一个空判断,当线程A执行完毕后,线程B抢到锁,就会再次执行new实例的代码,这时就创建了两个实例,就违背了单例模式的原则,而通过两次空判断就可以解决这个问题。
SingleTon2所示的单例模式是通过一个私有的内部类来实现的,这种方式相比前一种更加简单,并且还有一个好处,只有当instance被获取时才会加载内部类的这个单例,这种方式称为懒汉式加载。
SingleTon3所示的单例模式则是在类加载时就创建单例,在C#中可以放在静态构造函数中对instance进行初始化,也可以在声明instance变量时通过赋值表达式创建单例。
如果觉得这篇文章对你有帮助,请给作者点个赞吧!