深刻理解設計模式(一):單例模式

本文首先概述了單例模式,揭示了單例模式的應用場景和優缺點,最後咱們給出了單例模式的幾種實現方式及注意事項。web

1、什麼是單例模式

單例模式是一種經常使用的軟件設計模式,其定義是單例對象的類只能容許一個實例存在。數據庫

許多時候整個系統只須要擁有一個的全局對象,這樣有利於咱們協調系統總體的行爲。好比在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,而後服務進程中的其餘對象再經過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。windows

 

單例的實現主要是經過如下兩個步驟設計模式

  1. 將該類的構造方法定義爲私有方法,這樣其餘處的代碼就沒法經過調用該類的構造方法來實例化該類的對象,只有經過該類提供的靜態方法來獲得該類的惟一實例;
  2. 在該類內提供一個靜態方法,當咱們調用這個方法時,若是類持有的引用不爲空就返回這個引用,若是類保持的引用爲空就建立該類的實例並將實例的引用賦予該類保持的引用。

2、單例模式的應用場景

  舉一個小例子,在咱們的windows桌面上,咱們打開了一個回收站,當咱們試圖再次打開一個新的回收站時,Windows系統並不會爲你彈出一個新的回收站窗口。,也就是說在整個系統運行的過程當中,系統只維護一個回收站的實例。這就是一個典型的單例模式運用。安全

  繼續說回收站,咱們在實際使用中並不存在須要同時打開兩個回收站窗口的必要性。假如我每次建立回收站時都須要消耗大量的資源,而每一個回收站之間資源是共享的,那麼在沒有必要屢次重複建立該實例的狀況下,建立了多個實例,這樣作就會給系統形成沒必要要的負擔,形成資源浪費。服務器

  再舉一個例子,網站的計數器,通常也是採用單例模式實現,若是你存在多個計數器,每個用戶的訪問都刷新計數器的值,這樣的話你的實計數的值是難以同步的。可是若是採用單例模式實現就不會存在這樣的問題,並且還能夠避免線程安全問題。一樣多線程的線程池的設計通常也是採用單例模式,這是因爲線程池須要方便對池中的線程進行控制多線程

  一樣,對於一些應用程序的日誌應用,或者web開發中讀取配置文件都適合使用單例模式,如HttpApplication 就是單例的典型應用。性能

  從上述的例子中咱們能夠總結出適合使用單例模式的場景和優缺點:  網站

   適用場景: spa

  • 1.須要生成惟一序列的環境
  • 2.須要頻繁實例化而後銷燬的對象。
  • 3.建立對象時耗時過多或者耗資源過多,但又常常用到的對象。 
  • 4.方便資源相互通訊的環境

3、單例模式的優缺點

優勢

  • 在內存中只有一個對象,節省內存空間;

  • 避免頻繁的建立銷燬對象,能夠提升性能;

  • 避免對共享資源的多重佔用,簡化訪問;

  • 爲整個系統提供一個全局訪問點。

缺點

  •  不適用於變化頻繁的對象;

  • 濫用單例將帶來一些負面問題,如爲了節省資源將數據庫鏈接池對象設計爲的單例類,可能會致使共享鏈接池對象的程序過多而出現鏈接池溢出;

  • 若是實例化的對象長時間不被利用,系統會認爲該對象是垃圾而被回收,這可能會致使對象狀態的丟失;

4、單例模式的實現

1.餓漢式

// 餓漢式單例
public class Singleton1 {
 
    // 指向本身實例的私有靜態引用,主動建立
    private static Singleton1 singleton1 = new Singleton1();
 
    // 私有的構造方法
    private Singleton1(){}
 
    // 以本身實例爲返回值的靜態的公有方法,靜態工廠方法
    public static Singleton1 getSingleton1(){
        return singleton1;
    }
}

咱們知道,類加載的方式是按需加載,且加載一次。。所以,在上述單例類被加載時,就會實例化一個對象並交給本身的引用,供系統使用;並且,因爲這個類在整個生命週期中只會被加載一次,所以只會建立一個實例,即可以充分保證單例。

 

優勢:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。

缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。若是從始至終從未使用過這個實例,則會形成內存的浪費。

2.懶漢式

// 懶漢式單例
public class Singleton2 {
 
    // 指向本身實例的私有靜態引用
    private static Singleton2 singleton2;
 
    // 私有的構造方法
    private Singleton2(){}
 
    // 以本身實例爲返回值的靜態的公有方法,靜態工廠方法
    public static Singleton2 getSingleton2(){
        // 被動建立,在真正須要使用時纔去建立
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

咱們從懶漢式單例能夠看到,單例實例被延遲加載,即只有在真正使用的時候纔會實例化一個對象並交給本身的引用。

這種寫法起到了Lazy Loading的效果,可是隻能在單線程下使用。若是在多線程下,一個線程進入了if (singleton == null)判斷語句塊,還將來得及往下執行,另外一個線程也經過了這個判斷語句,這時便會產生多個實例。因此在多線程環境下不可以使用這種方式。

3.雙重加鎖機制

public class Singleton
    {
        private static Singleton instance;
        //程序運行時建立一個靜態只讀的進程輔助對象
        private static readonly object syncRoot = new object();
        private Singleton() { }
        public static Singleton GetInstance()
        {
            //先判斷是否存在,不存在再加鎖處理
            if (instance == null)
            {
                //在同一個時刻加了鎖的那部分程序只有一個線程能夠進入
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

Double-Check概念對於多線程開發者來講不會陌生,如代碼中所示,咱們進行了兩次if (singleton == null)檢查,這樣就能夠保證線程安全了。這樣,實例化代碼只用執行一次,後面再次訪問時,判斷if (singleton == null),直接return實例化對象。

使用雙重檢測同步延遲加載去建立單例的作法是一個很是優秀的作法,其不但保證了單例,並且切實提升了程序運行效率

優勢:線程安全;延遲加載;效率較高。

4.靜態初始化

//阻止發生派生,而派生可能會增長實例
    public sealed class Singleton
    {
        //在第一次引用類的任何成員時建立實例,公共語言運行庫負責處理變量初始化
        private static readonly Singleton instance=new Singleton();
        
        private Singleton() { }
        public static Singleton GetInstance()
        {
            return instance;
        }
    }

在實際應用中,C#與公共語言運行庫也提供了一種「靜態初始化」方法,這種方法不須要開發人員顯式地編寫線程安全代碼,便可解決多線程環境下它是不安全的問題。

5、總結

固然,單例模式的實現方法還有不少。可是,這四種是比較經典的實現,也是咱們應該掌握的幾種實現方式。

從這四種實現中,咱們能夠總結出,要想實現效率高的線程安全的單例,咱們必須注意如下兩點:

  • 儘可能減小同步塊的做用域;

  • 儘可能使用細粒度的鎖。

相關文章
相關標籤/搜索