天天看點

設計模式之單例模式

單例模式簡介

單例模式是GOF 23個設計模式中最簡單的模式了,它提供了一種建立唯一對象的最佳實作,注意此處的簡單隻是表述和意圖很簡單,但是實作起來,尤其是實作一個優美的單例模式卻沒有那麼簡單。

單例模式歸根結底就是要確定一個類隻有一個執行個體,并提供一個全局方式來通路該執行個體。具體而言,這種模式涉及到一個類,并由這個類建立自己的對象,同時確定隻有單個對象被建立,并提供唯一一種方式來通路該對象的執行個體。

在現實生活中,單例的場景有很多,比如一夫一妻制(當然不道德的除外),比如一個部門隻有一個上司等等。

單例模式UML類圖

設計模式之單例模式

 如上圖所示:

1、單例類隻能有一個執行個體。

2、單例類必須自己建立自己的唯一執行個體。

3、單例類必須給所有其他對象提供這一執行個體。

4、構造函數是私有的。

範例

Double-Check

我們先看一個非常流行而又簡單的實作

1:  public sealed class Singleton      
2:  {      
3:      private static Singleton instance = null;      
4:      private static readonly object padlock = new object();      
5:         
6:      private Singleton()      
7:      {      
8:      }      
9:         
10:      public static Singleton Instance      
11:      {      
12:          get      
13:          {      
14:              if (instance == null)      
15:              {      
16:                  lock (padlock)      
17:                  {      
18:                      if (instance == null)      
19:                      {      
20:                          instance = new Singleton();      
21:                          //Do a heavy task      
22:                      }      
23:                  }      
24:              }      
25:              return instance;      
26:          }      
27:      }      
28:  }      

上述解決方案上,使用到了Double-Check方式,Double-Check方式可以說是盛名已久了,線程A與線程B在Null Check時同時通過,但是在Lock時,隻能進入一個線程,其他線程都要等着。

這種方式在Java中編寫單例模式的時候是失效的,具體原因我沒有去深究。這一塊記憶體屏障技術(Memory Barrier),不過這段涉及到底層操作,一般很難有人會顯式操作,而且這段的控制異常複雜。另外一點就是,如果單例過程中操作的是一個數組或者其他對象,那麼在執行個體化後如果需要進行指派等運算操作的,那麼其他線程在進行Null Check的時候就不會再次進入,如果其他線程調用了這個單例對象的某個屬性,這極有可能出現難以預測的bug。

單例模式加載資料到記憶體,那麼如果我們需要在使用的時候再去加載到記憶體,而不是一開始就加載到記憶體,這樣可以節省記憶體空間。接下來我們看一下如何通過懶加載方式實作單例模式。

靜态類

采用靜态類實作單例模式,這并不是一種完全的懶加載,但依然是線程安全的

1:  public sealed class Singleton      
2:  {      
3:      private static readonly Singleton instance = new Singleton();      
4:         
5:      static Singleton()      
6:      {      
7:         
8:      }      
9:         
10:      private Singleton()      
11:      {      
12:         
13:      }      
14:         
15:      public static Singleton Instance      
16:      {      
17:          get      
18:          {      
19:              return instance;      
20:          }      
21:      }      
22:  }      

C#中的靜态構造函數僅在建立類的執行個體或引用靜态成員時執行,并且每個AppDomain隻執行一次,因為每次都需要對新構造的類型執行這種檢查,是以這種方式要比Double-Check方式更快。然而,也有一些問題:

  • 它不像其他實作那樣懶惰。尤其是,如果您有執行個體以外的靜态成員,那麼對這些成員的第一個引用将涉及建立執行個體。這将在下一個實作中得到糾正。
  • 如果一個靜态構造函數調用另一個靜态構造函數,而另一個靜态構造函數再次調用第一個靜态構造函數,則會出現複雜情況。需要注意,靜态構造函數在一個循環中互相引用的後果。
  • 隻有當類型沒有被[beforefieldinit]标記時,.NET才能保證類型初始值設定項的惰性。不幸的是,C編譯器(至少在.NET 1.1運作時中提供)将沒有靜态構造函數的所有類型(即看起來像構造函數但被标記為靜态的塊)标記為beforefieldinit。需要注意beforefieldinit會影響性能,beforefieldinit的具體用法可以參見MSDN。

對于這個實作,許多人更喜歡擁有一個屬性,以防将來需要進一步的操作,并且JIT内聯可能使性能相同。另外有一種快捷方式就是,可以将執行個體設定為公共的靜态隻讀變量,不設定為屬性,這樣代碼的基本架構會顯得非常小。(注意,如果需要惰性,靜态構造函數本身仍然是必需的。)

内部類

采用内部類,這是一種完全的懶加載。

1:  public sealed class Singleton      
2:  {      
3:      private Singleton()      
4:      {      
5:         
6:      }      
7:         
8:      public static Singleton Instance { get { return Nested.instance; } }      
9:         
10:      private class Nested      
11:      {      
12:          static Nested()      
13:          {      
14:                
15:          }      
16:         
17:          internal static readonly Singleton instance = new Singleton();      
18:      }      
19:  }      

在這裡,嵌套類的靜态成員在第一次引用的時候會進行執行個體化操作,并且該引用隻在執行個體中發生。這意味着實作是完全懶惰的,但具有前一個實作的所有性能優勢。請注意,盡管嵌套類可以通路内部類的私有成員,但反過來卻不是,是以需要在此處對執行個體進行内部通路。不過,這并不會引發任何問題,因為類本身是私有的。不過此處貌似顯得有點複雜。

Lazy

那麼有沒有其他方式優雅而又安全的實作單例模式呢,答案是有的,那就是通過Lazy方式,Lazy方式可以擁有更高的性能,因為執行個體隻有在使用的時候才會真正建立對象,這就在很大程度上減少了記憶體的占用,當然,比較如果是比較簡單的單例建立,可以忽略這條不利影響。

Lazy自帶Double-Check,是線程安全的,他就像一個盾牌,在建立過程中,不管是建立簡單對象還是複雜對象,都不會允許其他線程使用尚未建立完成的對象,更多的Lazy使用,請參考MSDN。

1 public sealed class Singleton
 2 {
 3     private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
 4 
 5     public static Singleton Instance { get { return lazy.Value; } }
 6 
 7     private Singleton()
 8     {
 9 
10     }
11 }      

優點:

全局範圍内隻有一個執行個體,避免了記憶體消耗,以及執行個體頻繁的建立和銷毀

避免了對資源的多重占用,比如獨占式場景中

缺點:

一旦對象指向的外部環境發生了變化,比如在網絡調用、MQ等場景中一般可以可以采用單例,但是這裡需要提醒的是,如果DNS發生異常,在異常期間将會出現極難修複的情況,除非手動重新開機并指向新的域伺服器

這一點有點違反單一職責原則,通常情況下,一個類應該隻關注自身邏輯而不是建立對象

沒有接口,無法繼承

本文參考了https://csharpindepth.com/articles/Singleton,該文也是深入了解C#的作者所寫,可以收藏此網站以便更快的擷取相關資訊。

  • 以上為本篇文章的主要内容,希望大家多提意見,如果喜歡記得點個推薦哦

    作者:

    艾心

    出處:

    https://www.cnblogs.com/edison0621/

    本文版權歸作者和部落格園共有,歡迎轉載,轉載時保留原作者和文章位址即可。

  • 繼續閱讀