單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬于創建型模式,它提供了一種創建對象的最佳方式。
舉個常見的單例模式例子,我們日常使用的電腦上都有一個回收站,在整個操作系統中,回收站只能有一個實例,整個系統都使用這個唯一的實例,而且回收站自行提供自己的實例。因此回收站是單例模式的應用。
一、單例模式概念
單例模式(Singleton Pattern):確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。單例模式是一種對象創建型模式
。
二、單例模式結構圖
單例模式是結構最簡單的設計模式之一,在它的核心結構中只包含一個被稱為單例類的特殊類。
單例模式有三個特性:
- 單例類只能有一個實例
- 單例類必須自行創建自己的唯一的實例
- 單例類必須給所有其他對象提供這一實例
單例模式結構如圖所示:
單例模式結構圖中只包含一個單例角色:
Singleton
(單例):在單例類的內部實現只生成一個實例,同時它提供一個靜態的getInstance()
工廠方法,讓客戶可以訪問它的唯一實例;為了防止在外部對其實例化,將其構造函數設計為私有
;在單例類內部定義了一個Singleton
類型的靜態
對象,作為外部共享的唯一實例。
三、單例模式的幾種實現方式
單例模式的實現有多種方式,如下所示:
1、懶漢式,線程不安全
是否 Lazy 初始化:是
是否多線程安全:否
實現難度:易
描述:這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因為沒有加鎖 synchronized
,所以嚴格意義上它并不算單例模式。這種方式 lazy loading
很明顯,不要求線程安全,在多線程不能正常工作。
代碼實例:
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
接下來介紹的幾種實現方式都支持多線程,但是在性能上有所差異。
2、懶漢式,線程安全
是否 Lazy 初始化:是
是否多線程安全:是
實現難度:易
描述:這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。
優點:第一次調用才初始化,避免內存浪費。
缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。getInstance() 的性能對應用程序不是很關鍵(該方法使用不太頻繁)。
代碼實例:
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
3、餓漢式
是否 Lazy 初始化:否
是否多線程安全:是
實現難度:易
描述:這種方式比較常用,但容易產生垃圾對象。
優點:沒有加鎖,執行效率會提高。
缺點:類加載時就初始化,浪費內存。它基于classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到lazy loading的效果。
代碼實例:
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
4、雙檢鎖/雙檢查鎖(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多線程安全:是
實現難度:較復雜
描述:這種方式稱為雙重檢查鎖(Double-Check Locking),需要注意的是,如果使用雙重檢查鎖定來實現懶漢式單例類,需要在靜態成員變量instance之前增加修飾符volatile,被volatile修飾的成員變量可以確保多個線程都能夠正確處理,且該代碼只能在JDK 1.5及以上版本中才能正確執行。由于volatile關鍵字會屏蔽Java虛擬機所做的一些代碼優化,可能會導致系統運行效率降低,因此即使使用雙重檢查鎖定來實現單例模式也不是一種完美的實現方式。
代碼實例:
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
5、靜態內部類
是否 Lazy 初始化:是
是否多線程安全:是
實現難度:一般
描述:餓漢式單例類不能實現延遲加載,不管將來用不用始終占據內存;懶漢式單例類線程安全控制煩瑣,而且性能受影響。可見,無論是餓漢式單例還是懶漢式單例都存在這樣那樣的問題,有沒有一種方法,能夠將兩種單例的缺點都克服,而將兩者的優點合二為一呢?答案是:Yes!下面我們來學習這種更好的被稱之為Initialization Demand Holder (IoDH)的技術。在IoDH中,我們在單例類中增加一個靜態(static)內部類,在該內部類中創建單例對象,再將該單例對象通過getInstance()方法返回給外部使用。由于靜態單例對象沒有作為Singleton的成員變量直接實例化,因此類加載時不會實例化Singleton,第一次調用getInstance()時將加載內部類SingletonHolder,在該內部類中定義了一個static類型的變量instance,此時會首先初始化這個成員變量,由Java虛擬機來保證其線程安全性,確保該成員變量只能初始化一次。由于getInstance()方法沒有任何線程鎖定,因此其性能不會造成任何影響。通過使用IoDH,我們既可以實現延遲加載,又可以保證線程安全,不影響系統性能,不失為一種最好的Java語言單例模式實現方式**(其缺點是與編程語言本身的特性相關,很多面向對象語言不支持IoDH)。
代碼實例:
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
6、枚舉
是否 Lazy 初始化:否
是否多線程安全:是
實現難度:易
描述:這種實現方式還沒有被廣泛采用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。
這種方式是Effective Java作者Josh Bloch提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。不過,由于 JDK1.5 之后才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。
不能通過reflection attack來調用私有構造方法。
代碼實例:
public enum Singleton { INSTANCE; public void whateverMethod() { } }
經驗之談:一般情況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 3 種餓漢方式。只有在要明確實現lazy loading
效果時,才會使用第 5 種登記方式。如果涉及到反序列化創建對象時,可以嘗試使用第 6 種枚舉方式。如果有其他特殊的需求,可以考慮使用第 4 種雙檢鎖方式。
四、Java語言中的單例模式
Java語言中就有很多單例模式的應用實例,這里舉例一個:Java的Runtime對象。
在Java語言內部,java.lang.Runtime
對象就是一個使用單例模式的例子。在每一個Java應用程序里面,都有唯一的一個Runtime
對象,應用程序可以與其運行環境發生相互作用。
Runtime
類提供一個靜態工廠方法getRuntime():
public static Runtime getRuntime();
通過調用此方法,可以獲得Runtime
類唯一的一個實例:
Runtime rt=Runtime.getRuntime();
五、總結
單例模式作為一種目標明確、結構簡單、理解容易的設計模式,在軟件開發中使用頻率相當高,在很多應用軟件和框架中都得以廣泛應用。
1.主要優點
單例模式的主要優點如下:
- 單例模式提供了對唯一實例的受控訪問。因為單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
- 由于在系統內存中只存在一個對象,因此可以節約系統資源,對于一些需要頻繁創建和銷毀的對象單例模式無疑可以提高系統的性能。
- 允許可變數目的實例。基于單例模式我們可以進行擴展,使用與單例控制相似的方法來獲得指定個數的對象實例,既節省系統資源,又解決了單例單例對象共享過多有損性能的問題。
2.主要缺點
單例模式的主要缺點如下:
- 由于單例模式中沒有抽象層,因此單例類的擴展有很大的困難。
- 單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的創建和產品的本身的功能融合到一起。
- 現在很多面向對象語言(如Java、C#)的運行環境都提供了自動垃圾回收的技術,因此,如果實例化的共享對象長時間不被利用,系統會認為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實例化,這將導致共享的單例對象狀態的丟失。
3.適用場景
在以下情況下可以考慮使用單例模式:
- 系統只需要一個實例對象,如系統要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許創建一個對象。
- 客戶調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。
所謂單例,指的就是單實例,有且僅有一個類實例,這個單例不應該由人來控制,而應該由代碼來限制,強制單例。
單例有其獨有的使用場景,一般是對于那些業務邏輯上限定不能多例只能單例的情況,例如:類似于計數器之類的存在,一般都需要使用一個實例來進行記錄,若多例計數則會不準確。
其實單例就是那些很明顯的使用場合,沒有之前學習的那些模式所使用的復雜場景,只要你需要使用單例,那你就使用單例,簡單易理解。
所以我認為有關單例模式的重點不在于場景,而在于如何使用。
1、常見的單例模式有兩種創建方式:所謂餓懶漢式與餓漢式
(1)懶漢式
何為懶?顧名思義,就是不做事,這里也是同義,懶漢式就是不在系統加載時就創建類的單例,而是在第一次使用實例的時候再創建。
詳見下方代碼示例:
public class LHanDanli { //定義一個私有類變量來存放單例,私有的目的是指外部無法直接獲取這個變量,而要使用提供的公共方法來獲取 private static LHanDanli dl = null; //定義私有構造器,表示只在類內部使用,亦指單例的實例只能在單例類內部創建 private LHanDanli(){} //定義一個公共的公開的方法來返回該類的實例,由于是懶漢式,需要在第一次使用時生成實例,所以為了線程安全,使用synchronized關鍵字來確保只會生成單例 public static synchronized LHanDanli getInstance(){ if(dl == null){ dl = new LHanDanli(); } return dl; } }
(2)餓漢式
又何為餓?餓者,饑不擇食;但凡有食,必急食之。此處同義:在加載類的時候就會創建類的單例,并保存在類中。
詳見下方代碼示例:
public class EHanDanli { //此處定義類變量實例并直接實例化,在類加載的時候就完成了實例化并保存在類中 private static EHanDanli dl = new EHanDanli(); //定義無參構造器,用于單例實例 private EHanDanli(){} //定義公開方法,返回已創建的單例 public static EHanDanli getInstance(){ return dl; } }
2、雙重加鎖機制
何為雙重加鎖機制?
在懶漢式實現單例模式的代碼中,有使用synchronized關鍵字來同步獲取實例,保證單例的唯一性,但是上面的代碼在每一次執行時都要進行同步和判斷,無疑會拖慢速度,使用雙重加鎖機制正好可以解決這個問題:
public class SLHanDanli { private static volatile SLHanDanli dl = null; private SLHanDanli(){} public static SLHanDanli getInstance(){ if(dl == null){ synchronized (SLHanDanli.class) { if(dl == null){ dl = new SLHanDanli(); } } } return dl; } }
看了上面的代碼,有沒有感覺很無語,雙重加鎖難道不是需要兩個synchronized進行加鎖的嗎?
其實不然,這里的雙重指的的雙重判斷,而加鎖單指那個synchronized,為什么要進行雙重判斷,其實很簡單,第一重判斷,如果單例已經存在,那么就不再需要進行同步操作,而是直接返回這個實例,如果沒有創建,才會進入同步塊,同步塊的目的與之前相同,目的是為了防止有兩個調用同時進行時,導致生成多個實例,有了同步塊,每次只能有一個線程調用能訪問同步塊內容,當第一個搶到鎖的調用獲取了實例之后,這個實例就會被創建,之后的所有調用都不會進入同步塊,直接在第一重判斷就返回了單例。至于第二個判斷,個人感覺有點查遺補漏的意味在內(期待高人高見)。
不論如何,使用了雙重加鎖機制后,程序的執行速度有了顯著提升,不必每次都同步加鎖。
其實我最在意的是volatile的使用,volatile關鍵字的含義是:被其所修飾的變量的值不會被本地線程緩存,所有對該變量的讀寫都是直接操作共享內存來實現,從而確保多個線程能正確的處理該變量。該關鍵字可能會屏蔽掉虛擬機中的一些代碼優化,所以其運行效率可能不是很高,所以,一般情況下,并不建議使用雙重加鎖機制,酌情使用才是正理!
3、類級內部類方式
餓漢式會占用較多的空間,因為其在類加載時就會完成實例化,而懶漢式又存在執行速率慢的情況,雙重加鎖機制呢?又有執行效率差的毛病,有沒有一種完美的方式可以規避這些毛病呢?
貌似有的,就是使用類級內部類結合多線程默認同步鎖,同時實現延遲加載和線程安全。
public class ClassInnerClassDanli { public static class DanliHolder{ private static ClassInnerClassDanli dl = new ClassInnerClassDanli(); } private ClassInnerClassDanli(){} public static ClassInnerClassDanli getInstance(){ return DanliHolder.dl; } }
如上代碼,所謂類級內部類,就是靜態內部類,這種內部類與其外部類之間并沒有從屬關系,加載外部類的時候,并不會同時加載其靜態內部類,只有在發生調用的時候才會進行加載,加載的時候就會創建單例實例并返回,有效實現了懶加載(延遲加載),至于同步問題,我們采用和餓漢式同樣的靜態初始化器的方式,借助JVM來實現線程安全。
其實使用靜態初始化器的方式會在類加載時創建類的實例,但是我們將實例的創建顯式放置在靜態內部類中,它會導致在外部類加載時不進行實例創建,這樣就能實現我們的雙重目的:延遲加載和線程安全。
4、使用
在Spring中創建的Bean實例默認都是單例模式存在的。
文章列表