前言
這篇來源我的公眾號,如果你沒看過,剛好直接看看,如果看過了也可以再看看,我稍微修改了一些內容,今天講解的內容如下:
一、什麼是單例模式
【單例模式】,英文名稱:Singleton Pattern,這個模式很簡單,一個類型只需要一個實例,他是屬於創建類型的一種常用的軟體設計模式。透過單例模式的方法所建立的類別在目前行程中只有一個實例(根據需要,也有可能在一個執行緒中屬於單例,如:僅在執行緒上下文內使用同一個實例)。
(推薦影片:java影片教學#)
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 => 32 + (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 => 32 + (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 = 0; i < 3; 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 => 32 + (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 => 32 + (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 = 0; i < 3; i++) { var th = new Thread( new ParameterizedThreadStart((state) => { //WeatherForecast.GetInstance(); // 此刻的心情 Console.WriteLine(_feeling.Date); }) ); th.Start(i); } return null; }
测试的结果,情理之中,只在我们项目初始化服务的时候,进入了一次构造函数:
和我們上邊說的是一樣的, Singleton是一種單例,而且還是雙檢鎖那種, 因為結論可以看出,我們使用單例模式,直接可以使用依賴注入Sigleton 就能滿足的,很方便。
四、單例模式的優缺點
【優】、單例模式的優點:
(1)、保證唯一性:防止其他物件實例化,保證實例的唯一性;
(2)、全域性:定義好資料後,可以再整個專案種的任何地方使用目前實例,以及資料;
【劣】、單例模式的缺點:
(1)、內存常駐:因為單例的生命週期最長,存在整個開發系統內,如果一直添加數據,或者是常駐的話,會造成一定的記憶體消耗。
以下內容來自百度百科:
優點
#一、實例控制
單例模式會阻止其他物件實例化其自己的單例物件的副本,從而確保所有物件都存取唯一實例。
二、靈活性
因為類別控制了實例化過程,所以類別可以靈活地更改實例化過程。
缺點
一、開銷
雖然數量很少,但如果每次物件要求引用時都要檢查是否存在類別的實例,將仍然需要一些開銷。可以透過使用靜態初始化解決此問題。
二、可能的開發混淆
使用單例物件(尤其在類別庫中定義的物件)時,開發人員必須記住自己不能使用new關鍵字實例化物件。因為可能無法存取庫原始碼,因此應用程式開發人員可能會意外發現自己無法直接實例化此類。
三、物件生存期
不能解決刪除單一物件的問題。在提供記憶體管理的語言中(例如基於.NET Framework的語言),只有單例類別能夠導致實例被取消分配,因為它包含對該實例的私有參考。在某些語言中(如 C ),其他類別可以刪除物件實例,但這樣會導致單例類別中出現懸浮引用。
本文來自php中文網,java教學欄目,歡迎學習!
以上是java中單例模式與Singleton的詳細內容。更多資訊請關注PHP中文網其他相關文章!