深入理解Java設計模式之單例模式

一、什麼是單例模式

單例模式是一種常用的軟件設計模式,其定義是單例對象的類隻能允許一個實例存在。

許多時候整個系統隻需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化瞭在復雜環境下的配置管理。

單例的實現主要是通過以下兩個步驟:

1.將該類的構造方法定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,隻有通過該類提供的靜態方法來得到該類的唯一實例;

2.在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用。

二、單例模式的應用場景

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

繼續說回收站,我們在實際使用中並不存在需要同時打開兩個回收站窗口的必要性。假如我每次創建回收站時都需要消耗大量的資源,而每個回收站之間資源是共享的,那麼在沒有必要多次重復創建該實例的情況下,創建瞭多個實例,這樣做就會給系統造成不必要的負擔,造成資源浪費。

再舉一個例子,網站的計數器,一般也是采用單例模式實現,如果你存在多個計數器,每一個用戶的訪問都刷新計數器的值,這樣的話你的實計數的值是難以同步的。但是如果采用單例模式實現就不會存在這樣的問題,而且還可以避免線程安全問題。同樣多線程的線程池的設計一般也是采用單例模式,這是由於線程池需要方便對池中的線程進行控制

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

從上述的例子中我們可以總結出適合使用單例模式的場景和優缺點:

適用場景:

1.需要生成唯一序列的環境

2.需要頻繁實例化然後銷毀的對象。

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#與公共語言運行庫也提供瞭一種“靜態初始化”方法,這種方法不需要開發人員顯式地編寫線程安全代碼,即可解決多線程環境下它是不安全的問題。

五、總結

當然,單例模式的實現方法還有很多。但是,這四種是比較經典的實現,也是我們應該掌握的幾種實現方式。

從這四種實現中,我們可以總結出,要想實現效率高的線程安全的單例,我們必須註意以下兩點:

盡量減少同步塊的作用域;

盡量使用細粒度的鎖。

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: