单例模式是最常见的设计模式之一,大家应该都不陌生了,但是让大家立即写个单例模式的代码,或说出项目中哪些场景使用了单例,估计大家不一定能立即完全正确,今天我们接来学习下单例模式的相关知识以及实战。
从本文你能Get到哪些?
更加深刻的理解单例模式;
单例模式常见的3种写法;
单例模式的使用场景。
23种设计模式在写代码之前,需要考虑下该设计模式的使用场景,单例模式也是如此。首先我们来想想,在实际的项目开发过程中,有哪些场景是使用单例模式的?
我来举几个例子:
1、以前ado.net连接数据库的连接操作,用的是单例;如下代码:
public static string connectionString = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;
(注:.NetCore的EFCore连接数据库的AddDbContextPool不是单例模式,而是Scope模式,因为使用到事务等原因,以后细说。)
2、连接redis的操作,使用单例, 3、.NetCore的依赖注入的作用域工厂对象IServiceScopeFactory 是单例模式;
services.AddSingleton(redisConfig) .AddSingleton<IRedisCacheClient, RedisCacheClient>() //其他代码省略
3、.NetCore的依赖注入的作用域工厂对象IServiceScopeFactory 是单例模式;
4、IHttpClientFactory 默认是一个单例。
官方地址:
https://docs.microsoft.com/zh-cn/dotnet/api/system.net.http.ihttpclientfactory?view=dotnet-plat-ext-5.0
5、MQ的消费者端依赖注入使用单例模式。
根据上面的几个例子,我们来作一个总结!
1、定义:
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。保证进程中只有一个实例。
2、场景:
程序只需要该对象仅实例化一次。
场景解释:
先好好体会下刚刚举的例子,比如第3个例子,依赖注入的容器对象只能是一个实例,不然程序就根据无法找到容器里的服务信息。比如第1和2个例子,程序连接数据库或Redis,其创建和销毁连接的代价非常大,若不用单例,程序很容易被拖垮。另外,程序连接数据库或Redis,程序就相当于其一个客户端,客户端只需要一个实例就够了。
3、误区:
1、为了提升性能而使用单例模式;
2、为了解决多线程并发问题而使用单例模式;
误区解释:
提升性能与单例模式没有任何关联关系,使用单例模式可能会提升性能也可能会降低性能,也可能无任何性能影响。其实23种设计模式的设计初衷跟性能都不搭边。
多线程并发问题也非使用单例的必要条件。
代码实战
接下来我们来一边写代码一边讲解。
单例模式的实现或写法一般分2种,懒汉式和饿汉式。
懒汉模式
懒汉式是最基本的实现方法,写法就是Lock加判断是否有实例,有则立即返回,没有则创建后再返回。
写法分为3个步骤:
第一步,私有化构造函数,防止调用方手动实例化;
第二步,对外提供一个获取实例的方法,这个方法一般为公开的静态方法;
第三步,返回共用的一个静态对象/字段。
根据这三步骤,我们来写一个简单的实现:
/// <summary> /// 懒汉模式的单例模式,最简单的实现,未加锁 /// </summary> public class MySingletonSimple { private MySingletonSimple() { } private static MySingletonSimple _mySingleton; public static MySingletonSimple CreateInstance() { if (_mySingleton == null) { _mySingleton = new MySingletonSimple(); } return _mySingleton; } }
测试下该方法:
static void Main(string[] args) { for (int i = 0; i < 100; i++) { MySingletonSimple mySignletonSimple = MySingletonSimple.CreateInstance(); //对象的哈希码HashCode是标识对象在堆里的一个唯一标识。我们可以根据哈希码来确定一组对象是否为相同的对象。 Console.WriteLine($"mySignletonSimple的hashCode={ mySignletonSimple.GetHashCode()}"); } }
这里我们做一个循环100次,每次都调用MySingletonSimple.CreateInstance(),并且我们用对象的哈希码来验证对象是否为同一个对象。
(对象的哈希码HashCode是标识对象在堆里的一个唯一标识。我们可以根据哈希码来确定一组对象是否为相同的对象。)
结果如下:
说明上面的代码没问题,但是,我们这里都在同一线程里,若使用多线程,则出现问题了。比如以下代码:
static void Main(string[] args) { for (int i = 0; i < 100; i++) { Task.Run(() => { MySingletonSimple mySignletonSimple = MySingletonSimple.CreateInstance(); //对象的哈希码HashCode是标识对象在堆里的一个唯一标识。我们可以根据哈希码来确定一组对象是否为相同的对象。 Console.WriteLine($"mySignletonSimple的hashCode={ mySignletonSimple.GetHashCode()}"); }); }
使用Task.Run开启多线程。执行结果如下:
很明显,前面几条的哈希码都不一致,后面的基本上都一致,因为创建已经创建好了。
所以这种写法是非线程安全的,我们需要加个锁来确保线程安全。(双检锁)
/// <summary> /// 懒汉模式的单例模式,最简单的实现,加锁 /// </summary> public class MySingletonSimple { private MySingletonSimple() { } private static MySingletonSimple _mySingleton; private static readonly object MyLock = new object(); public static MySingletonSimple CreateInstance() { if (_mySingleton == null) { lock (MyLock) { if (_mySingleton == null) { _mySingleton = new MySingletonSimple(); } } } return _mySingleton; } }
加了1个lock锁,注意if在外面和里面都要加一下,外层的if是提升性能,不让每次执行都去锁那边等待。(双检锁)
懒汉模式之所以叫懒汉模式,是因为我们在使用单例的类时候,都必须手动调用获取实例的方法,才能实现单例。若该单例类里其他方法,比如B,我们又不想每次都调用获取实例的方法,再调用B,这个时候就要使用饿汉模式了。
饿汉模式
饿汉模式就是使用静态构造函数或者静态字段,天生是线程安全,所有就不需要锁了。
静态构造函数和静态字段都是由CLR来保障,在第一次使用到这个类之前,自动被执行,并且只执行一次。
/// <summary> /// 单例饿汉模式,使用静态构造函数,实现单例 /// </summary> public class MySingleton2 { private static MySingleton2 _mySingleton2; private MySingleton2() { } /// <summary> /// 静态构造函数:由CLR保证,在第一次使用到这个类型之前,自动被调用且只调用一次 /// 很多初始化都可以写在这里 /// </summary> static MySingleton2() { _mySingleton2 = new MySingleton2(); } public static void ShowTest() { Console.WriteLine($"我在饿汉模式里,_mySingleton2={_mySingleton2.GetHashCode()}"); } }
/// <summary> /// 单例饿汉模式,使用静态字段,实现单例 /// </summary> public class MySingleton3 { private static MySingleton3 _mySingleton3 = new MySingleton3(); private MySingleton3() { } public static void ShowTest() { Console.WriteLine($"我在饿汉模式里,_mySingleton3={_mySingleton3.GetHashCode()}"); } }
Lazy单例模式
使用Lazy实现单例模式
/// <summary> /// Lazy实现单例模式 /// </summary> public class MySingleton4 { public static MySingleton4 _mySingleton4 = new Lazy<MySingleton4>(() => new MySingleton4()).Value; // private static MySingleton4 _mySingleton4 = new MySingleton4(); //public MySingleton4() //{ //} public void ShowTest() { Console.WriteLine($"我在饿汉模式里,_mySingleton4={_mySingleton4.GetHashCode()}"); } }
测试:
for (int i = 0; i < 1000; i++) { Task.Run(() => { MySingleton4 _MySingleton4 = new MySingleton4(); _MySingleton4.ShowTest(); }); }
Lazy<T> 是一个非常好的延迟加载的特性,可以在使用到该变量的时候才真正进行参数实例化和一系列控制反转的操作。因为通常我们某一个Service中可能只有一部分的方法需要用到某个变量,但是这个变量又需要在构造函数中依赖注入,此时 Lazy<T> 将帮助提升效率。
当我们在 .net core 中运行如下代码注入 Lazy<T> 变量的时候:
public AccountService(Lazy<IHttpContextAccessor> httpContextAccessor) { }
之前需要在 Startup.cs 中的 public void ConfigureServices(IServiceCollection services) 方法中加入如下代码使之生效:
public void ConfigureServices(IServiceCollection services) { services.AddMvc();//默认会有 services.AddTransient(typeof(Lazy<>));//注册Lazy }