从壹开始 [ Design Pattern ] 之二 ║ 单例模式 与 Singleton
前言
这一篇来源我的公众号,如果你没看过,正好直接看看,如果看过了也可以再看看,我稍微修改了一些内容,今天讲解的内容如下:
一、什么是单例模式
【单例模式】,英文名称:Singleton Pattern,这个模式很简单,一个类型只需要一个实例,他是属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)。
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
那咱们大概知道了,其实说白了,就是我们整个项目周期内,只会有一个实例,当项目停止的时候,实例销毁,当重新启动的时候,我们的实例又会产品。
上文中说到了一个名词【创建类型】的设计模式,那什么是创建类型的设计模式呢?
创建型(Creational)模式:负责对象创建,我们使用这个模式,就是为了创建我们需要的对象实例的。
那除了创建型还有其他两种类型的模式:
结构型(Structural)模式:处理类与对象间的组合
行为型(Behavioral)模式:类与对象交互中的职责分
这两种设计模式,以后会慢慢说到,这里先按下不表。
咱们就重点从0开始分析分析如何创建一个单例模式的对象实例。
二、如何创建单例模式
实现单例模式有很多方法:从“懒汉式”到“饿汉式”,最后“双检锁”模式,这里咱们就慢慢的,从一步一步的开始讲解如何创建单例。
1、正常的思考逻辑顺序
既然要创建单一的实例,那我们首先需要学会如何去创建一个实例,这个很简单,相信每个人都会创建实例,就比如说这样的:
/// <summary>
/// 定义一个天气类
/// </summary>
public class WeatherForecast
{
public WeatherForecast()
{
Date = DateTime.Now;
}
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => + (int)(TemperatureC / 0.5556);
public string Summary { get; set; }
} [HttpGet]
public WeatherForecast Get()
{
// 实例化一个对象实例
WeatherForecast weather = new WeatherForecast();
return weather;
}
我们每次访问的时候,时间都是会变化,所以我们的实例也是一直在创建,在变化:
相信每个人都能看到这个代码是什么意思,不多说,直接往下走,我们知道,单例模式的核心目的就是:
必须保证这个实例在整个系统的运行周期内是唯一的,这样可以保证中间不会出现问题。
那好,我们改进改进,不是说要唯一一个么,好说!我直接返回不就行了:
/// <summary>
/// 定义一个天气类
/// </summary>
public class WeatherForecast
{
// 定义一个静态变量来保存类的唯一实例
private static WeatherForecast uniqueInstance; // 定义私有构造函数,使外界不能创建该类实例
private WeatherForecast()
{
Date = DateTime.Now;
}
/// <summary>
/// 静态方法,来返回唯一实例
/// 如果存在,则返回
/// </summary>
/// <returns></returns>
public static WeatherForecast GetInstance()
{
// 如果类的实例不存在则创建,否则直接返回
// 其实严格意义上来说,这个不属于【单例】
if (uniqueInstance == null)
{
uniqueInstance = new WeatherForecast();
}
return uniqueInstance;
}
public DateTime Date { get; set; }public int TemperatureC { get; set; }
public int TemperatureF => + (int)(TemperatureC / 0.5556);
public string Summary { get; set; }
}
然后我们修改一下调用方法,因为我们的默认构造函数已经私有化了,不允许再创建实例了,所以我们直接这么调用:
[HttpGet]
public WeatherForecast Get()
{
// 实例化一个对象实例
WeatherForecast weather = WeatherForecast.GetInstance();
return weather;
}
最后来看看效果:
这个时候,我们可以看到,时间已经不发生变化了,也就是说我们的实例是唯一的了,大功告成!是不是很开心!
但是,别着急,问题来了,我们目前是单线程的,所以只有一个,那如果多线程呢,如果多个线程同时访问,会不会也会正常呢?
这里我们做一个测试,我们在项目启动的时候,用多线程去调用:
[HttpGet]
public WeatherForecast Get()
{
// 实例化一个对象实例
//WeatherForecast weather = WeatherForecast.GetInstance(); // 多线程去调用
for (int i = ; i < ; i++)
{
var th = new Thread(
new ParameterizedThreadStart((state) =>
{
WeatherForecast.GetInstance();
})
);
th.Start(i);
}
return null;
}
然后我们看看效果是怎样的,按照我们的思路,应该是只会走一遍构造函数,其实不是:
3个线程在第一次访问GetInstance方法时,同时判断(uniqueInstance ==null)这个条件时都返回真,然后都去创建了实例,这个肯定是不对的。那怎么办呢,只要让GetInstance方法只运行一个线程运行就好了,我们可以加一个锁来控制他,代码如下:
public class WeatherForecast
{
// 定义一个静态变量来保存类的唯一实例
private static WeatherForecast uniqueInstance;
// 定义一个锁,防止多线程
private static readonly object locker = new object(); // 定义私有构造函数,使外界不能创建该类实例
private WeatherForecast()
{
Date = DateTime.Now;
}
/// <summary>
/// 静态方法,来返回唯一实例
/// 如果存在,则返回
/// </summary>
/// <returns></returns>
public static WeatherForecast GetInstance()
{
// 当第一个线程执行的时候,会对locker对象 "加锁",
// 当其他线程执行的时候,会等待 locker 执行完解锁
lock (locker)
{
// 如果类的实例不存在则创建,否则直接返回
if (uniqueInstance == null)
{
uniqueInstance = new WeatherForecast();
}
} return uniqueInstance;
}
public DateTime Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => + (int)(TemperatureC / 0.5556); public string Summary { get; set; }
}
这个时候,我们再并发测试,发现已经都一样了,这样就达到了我们想要的效果,但是这样真的是最完美的么,其实不是的,因为我们加锁,只是第一次判断是否为空,如果创建好了以后,以后就不用去管这个 lock 锁了,我们只关心的是 uniqueInstance 是否为空,那我们再完善一下:
/// <summary>
/// 定义一个天气类
/// </summary>
public class WeatherForecast
{
// 定义一个静态变量来保存类的唯一实例
private static WeatherForecast uniqueInstance;
// 定义一个锁,防止多线程
private static readonly object locker = new object(); // 定义私有构造函数,使外界不能创建该类实例
private WeatherForecast()
{
Date = DateTime.Now;
}
/// <summary>
/// 静态方法,来返回唯一实例
/// 如果存在,则返回
/// </summary>
/// <returns></returns>
public static WeatherForecast GetInstance()
{
// 当第一个线程执行的时候,会对locker对象 "加锁",
// 当其他线程执行的时候,会等待 locker 执行完解锁
if (uniqueInstance == null)
{
lock (locker)
{
// 如果类的实例不存在则创建,否则直接返回
if (uniqueInstance == null)
{
uniqueInstance = new WeatherForecast();
}
}
} return uniqueInstance;
}
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => + (int)(TemperatureC / 0.5556);
public string Summary { get; set; }
}
这样才最终的完美实现我们的单例模式!搞定。
2、幽灵事件:指令重排
当然,如果你看完了上边的那四步已经可以出师了,平时我们就是这么使用的,也是这么想的,但是真的就是万无一失么,有一个 JAVA 的朋友提出了这个问题,C# 中我没有听说过,是我孤陋寡闻了么:
单例模式的幽灵事件,时令重排会偶尔导致单例模式失效。
是不是听起来感觉很高大上,而不知所云,没关系,咱们平时用不到,但是可以了解了解:
为何要指令重排?
指令重排是指的 volatile,现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。
相反,流水线是并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段, 而两条加法指令在“执行”阶段就只能串行工作。
相比于串行+阻塞的方式,流水线像这样并行的工作,效率是非常高的。然而,这样一来,乱序可能就产生了。比如一条加法指令原本出现在一条除法指令的后面,但是由于除法的执行时间很长,在它执行完之前,加法可能先执行完了。再比如两条访存指令,可能由于第二条指令命中了cache而导致它先于第一条指令完成。
一般情况下,指令乱序并不是CPU在执行指令之前刻意去调整顺序。CPU总是顺序的去内存里面取指令,然后将其顺序的放入指令流水线。但是指令执行时的各种条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。
这个是从网上摘录的,大概意思看看就行,理解双检锁失效原因有两个重点
1、编译器的写操作重排问题.
例 : B b = new B();
上面这一句并不是原子性的操作,一部分是new一个B对象,一部分是将new出来的对象赋值给b.
直觉来说我们可能认为是先构造对象再赋值.但是很遗憾,这个顺序并不是固定的.再编译器的重排作用下,可能会出现先赋值再构造对象的情况.
2、结合上下文,结合使用情景.
理解了1中的写操作重排以后,我卡住了一下.因为我真不知道这种重排到底会带来什么影响.实际上是因为我看代码看的不够仔细,没有意识到使用场景.双检锁的一种常见使用场景就是在单例模式下初始化一个单例并返回,然后调用初始化方法的方法体内使用初始化完成的单例对象.
三、Singleton = 单例 ?
上边我们说了很多,也介绍了很多单例的原理和步骤,那这里问题来了,我们在学习依赖注入的时候,用到的 Singleton 的单例注入,是不是和上边说的一回事儿呢,这里咱们直接多多线程测试一下就行:
/// <summary>
/// 定义一个心情类
/// </summary>
public class Feeling
{
public Feeling()
{
Date = DateTime.Now;
}
public DateTime Date { get; set; }
} // 单例注册到容器内
services.AddSingleton<Feeling>();
这里重点表扬下评论区的@我是你帅哥 小伙伴,及时的发现了我文章的漏洞,笔芯!
紧接着我们就控制器注入服务,然后多线程测试:
private readonly ILogger<WeatherForecastController> _logger;
private readonly Feeling _feeling; public WeatherForecastController(ILogger<WeatherForecastController> logger, Feeling feeling)
{
_logger = logger;
_feeling = feeling;
} [HttpGet]
public WeatherForecast Get()
{
// 实例化一个对象实例
//WeatherForecast weather = WeatherForecast.GetInstance(); // 多线程去调用
for (int i = ; i < ; i++)
{
var th = new Thread(
new ParameterizedThreadStart((state) =>
{
//WeatherForecast.GetInstance(); // 此刻的心情
Console.WriteLine(_feeling.Date);
})
);
th.Start(i);
}
return null;
}
测试的结果,情理之中,只在我们项目初始化服务的时候,进入了一次构造函数:
四、单例模式的优缺点
【优】、单例模式的优点:
(1)、保证唯一性:防止其他对象实例化,保证实例的唯一性;
(2)、全局性:定义好数据后,可以再整个项目种的任何地方使用当前实例,以及数据;
【劣】、单例模式的缺点:
(1)、内存常驻:因为单例的生命周期最长,存在整个开发系统内,如果一直添加数据,或者是常驻的话,会造成一定的内存消耗。
以下内容来自百度百科:
优点
一、实例控制单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。二、灵活性因为类控制了实例化过程,所以类可以灵活更改实例化过程。缺点
一、开销虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题。二、可能的开发混淆使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。三、对象生存期不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于.NET Framework的语言),只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除对象实例,但这样会导致单例类中出现悬浮引用。
五、示例代码
https://github.com/anjoy8/DesignPattern/tree/master/SingletonPattern
从壹开始 [ Design Pattern ] 之二 ║ 单例模式 与 Singleton的更多相关文章
- 从壹开始 [ Design Pattern ] 之三 ║ 工厂模式 与 小故事
编者按: 定义一个用于创建对象的接口,让子类决定实例化哪一个类.工厂方法使得一个类的实例化延迟到子类. 工厂模式,是迄今为止,使用最多,最广泛的设计模式之一,它的身影几乎出现在每一个框架和个人代码之中 ...
- 从壹开始 [ Design Pattern ] 之一 ║ 设计模式开篇讲
缘起 不说其他的没用的开场白了,直接给大家分享三个小故事,都来自于我的读者粉丝(我厚着脸皮称为粉丝吧
- Design Pattern —— Singleton
Design Pattern —— Singleton 强力推荐枚举和类级内部类方式实现单例模式 单例模式是开发中非常常用的一种模式,简单的说,我们希望一个类永远都只有一个对象. 主要有两个用途: ...
- 说说设计模式~大话目录(Design Pattern)
回到占占推荐博客索引 设计模式(Design pattern)与其它知识不同,它没有华丽的外表,没有吸引人的工具去实现,它是一种心法,一种内功,如果你希望在软件开发领域有一种新的突破,一个质的飞越,那 ...
- 设计模式(Design Pattern)系列之.NET专题
最近,不是特别忙,重新翻了下设计模式,特地在此记录一下.会不定期更新本系列专题文章. 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结. 使用 ...
- [Design Pattern] Singleton Pattern 简单案例
Singleton Pattern, 即单例模式,用于获取类的一个对象,该对象在整个应用中是其类的唯一对象.单例模式属于创建类的设计模式. SingleObject 作为单例类,内含了一个静态私有的 ...
- python singleton design pattern super() 多继承
python singleton design pattern decorate baseclass metaclass import module super() 一.A decorator de ...
- 设计模式之一:单例模式(Singleton Pattern)
写这个系列的文章,只为把所学的设计模式再系统的整理一遍.错误和不周到的地方欢迎大家批评.点击这里下载源代码. 什么时候使用单例模式 在程序运行时,某种类型只需要一个实例时,一般采用单例模式.为什么需要 ...
- 巧用代理设计模式(Proxy Design Pattern)改善前端图片加载体验
这篇文章介绍一种使用代理设计模式(Proxy Design Pattern)的方法来改善您的前端应用里图片加载的体验. 假设我们的应用里需要显示一张尺寸很大的图片,位于远端服务器.我们用一些前端框架的 ...
随机推荐
- java读取存在src目录下和存在同级目录下的配置文件
如果我有个文件存在src下一级的地方和存在src同级的目录应该怎么用相对路径去获取如图: 一.如果存在src同级的地方应该是InputStream in = new BufferedInputStre ...
- 每日温度(LeetCode Medium难度算法题)题解
LeetCode 题号739中等难度 每日温度 题目描述: 根据每日 气温 列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数.如果之后都不会升高,请在该位置用 0 ...
- 百万年薪python之路 -- 基本数据类型练习
1.代码敲一遍,然后整理笔记 2.有变量name = "aleX leNb" 完成如下操作: 移除 name 变量对应的值两边的空格,并输出处理结果 name = "al ...
- Andriod Studio设置代码字体大小
- oc基本控件
(一)添加UIWindow UIWindow *window1=[[UIWindow alloc] init]; //window.frame=CGRectMake(10, 470, 100, 30) ...
- Python+requests+unittest+excel实现接口自动化测试框架(摘录)
一.框架结构: 工程目录 二.Case文件设计 三.基础包 base 3.1 封装get/post请求(runmethon.py) 1 import requests 2 import json 3 ...
- html获得当前日期
<html> <head> <title> </title> </head> <body> <!-- 获得当前日期(年月日 ...
- 《Effective Java》 读书笔记(五)使用依赖注入取代原本的资源依赖
相信接触过Spring的同学,对于依赖注入并不陌生. 刚开始在听说这个名字的时候,一直不明白到底什么叫依赖注入,后来才发现,依赖注入一直都存在我们日常代码中,只是我们没有刻意的把它提出来,然后再取这样 ...
- 学习笔记34_EF上下文管理
*上下文对象dbContext最好不要频繁的使用Using(var dbContext = new ....):那么就会产生过多的数据库交互:而且每个dbContext中村的数据,由于操作不同,数据可 ...
- F#周报2019年第45期
新闻 邀请博客主们:2019年的F# Advent日历 宣告ML.NET 1.4 .NET Core与Jupyter笔记本 在Jupyter笔记本中使用ML.NET 用于Windows桌面的.NET ...