目錄
本文的結構如下:
- 什么是單例模式
- 為什么要用該模式
- 模式的結構
- 代碼示例
- 優點和缺點
- 適用環境
- 模式應用
- 總結
一、前言
對于系統中的某些類來說,只有一個實例很重要,例如,Windows任務管理器。通常情況下,無論我們啟動任務管理多少次,Windows系統始終只能彈出一個任務管理器窗口,也就是說在一個Windows系統中,任務管理器存在唯一性。為什么要這樣設計呢?
其一,如果能彈出多個窗口,且這些窗口的內容完全一致,全部是重復對象,這勢必會浪費系統資源,任務管理器需要獲取系統運行時的諸多信息,這些信息的獲取需要消耗一定的系統資源,包括CPU資源及內存資源等,浪費是可恥的,而且根本沒有必要顯示多個內容完全相同的窗口;
其二,如果彈出的多個窗口內容不一致,問題就更加嚴重了,這意味著在某一瞬間系統資源使用情況和進程、服務等信息存在多個狀態,例如任務管理器窗口A顯示“CPU使用率”為10%,窗口B顯示“CPU使用率”為15%,到底哪個才是真實的呢?這會誤導用戶。
在實際開發中,也經常有類似的情況,為了節約系統資源,有時需要確保系統中某個類只有唯一一個實例,當這個唯一實例創建成功之后,就無法再創建一個同類型的其他對象,所有的操作都只能基于這個唯一實例。為了確保對象的唯一性,可以通過單例模式來實現。
二、什么是單例模式
單例模式相對簡單,它確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。
單例模式的要點有三個:
- 一是某個類只能有一個實例;
- 二是它必須自行創建這個實例;
- 三是它必須自行向整個系統提供這個實例。
單例模式是一種對象創建型模式。單例模式又名單件模式或單態模式。
三、為什么要用該模式
前言中已經介紹過,在開發軟件中,可能有些類的創建十分消耗系統資源,或者初始化時間較長,像線程池,數據庫連接池,秉著節約的態度,為了提升軟件的性能,一般只允許這些類只被創建出一個實例,且能夠全局共享。
四、模式的結構
單例模式的結構簡單,只有一個類。
五、代碼示例
單例模式結構雖然簡單,但代碼里面還是有許多道道,有好幾種寫法,下面具體看看:
5.1、餓漢式
/**
* Created by w1992wishes on 2017/11/2.
*/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
這種方式和名字很貼切,饑不擇食,在類裝載的時候就創建,不管用不用,先創建了再說。
缺點是:如果一直沒有被使用,便浪費了空間,典型的空間換時間,如果初始化很耗費時間,同時也會推遲系統的啟動時間。
優點是:每次調用的時候,就不需要再判斷,節省了運行時間。
java Runtime使用的就是“餓漢式”單例:
5.2、懶漢式(非線程安全)
/**
* Created by w1992wishes on 2017/11/2.
*/
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
懶漢模式申明了一個靜態對象,在用戶第一次調用時初始化。
缺點是:第一次加載時需要實例化,反映稍慢一些,而且在多線程不能正常工作,很可能會造成多次實例化,就不再是單例了。
優點是:使用時才創建,節約了資源。
“懶漢式”與“餓漢式”的最大區別就是將單例的初始化操作,延遲到需要的時候才進行,這樣做在某些場合中有很大用處。比如某個單例用的次數不是很多,但是這個單例提供的功能又非常復雜,而且加載和初始化要消耗大量的資源,這個時候使用“懶漢式”就是非常不錯的選擇。
5.3、懶漢式(線程安全)
/**
* Created by w1992wishes on 2017/11/2.
*/
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
在“懶漢式(非線程安全)”的基礎上加上一個synchronized修飾,就是線程安全的了。
缺點是:其實只有第一次調用getInstance時才真正需要同步,一旦設置好instance實例后,之后每次同步都是累贅,平白增添性能消耗。
優點是:可以在多線程環境下確保只初始化一個單例。
5.4、雙重校驗鎖(DCL)
/**
* 注意此處使用的關鍵字 volatile,它保證了可見性
* 被volatile修飾的變量的值,將不會被本地線程緩存,
* 所有對該變量的讀寫都是直接操作共享內存,從而確保多個線程能正確的處理該變量。
* Created by w1992wishes on 2017/11/2.
*/
public class Singleton {
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
如果程序可以接受synchronized帶來的性能負擔,“懶漢式(線程安全)”可以使用,但如果很關心性能,“雙重校驗鎖(DCL)”將會帶來很大幫助。
在getInstance()方法中對instance進行了兩次判空,第一次是為了不必要的同步,因為只有第一次調用getInstance()方法才需要同步,第二次是在singleton等于null的情況下才創建實例。
缺點是:代碼相對其他較為復雜,有一定的性能損耗。
優點是:既可以達到線程安全,也可以使性能不受很大的影響,換句話說在保證線程安全的前提下,既節省空間也節省了時間。
5.5、靜態內部類
/**
* Created by w1992wishes on 2017/11/2.
*/
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder{
private static final Singleton sInstance = new Singleton();
}
}
缺點是:與編程語言本身的特性相關,很多面向對象語言不支持。
優點是:由于靜態單例對象沒有作為Singleton的成員變量直接實例化,因此類加載時不會實例化Singleton,第一次調用getInstance()時將加載內部類SingletonHolder,在該內部類中定義了一個static類型的變量sInstance,此時會首先初始化這個成員變量,由Java虛擬機來保證其線程安全性,確保該成員變量只能初始化一次。由于getInstance()方法沒有任何線程鎖定,因此其性能不會造成任何影響。
5.6、枚舉
/**
* Created by w1992wishes on 2017/11/2.
*/
public enum Singleton {
INSTANCE;//定義一個枚舉的元素,它就是 Singleton 的一個實例
public void doSomething(){}
}
使用如下:
public static void main(String args[]) {
Singleton singleton = Singleton.instance;
singleton.doSomeThing();
}
缺點是:大部分應用開發很少用枚舉,可讀性并不是很高。
優點是:使用枚舉來實現單實例控制會更加簡潔,而且無償地提供了序列化機制,并由JVM從根本上提供保障,絕對防止多次實例化,是更簡潔、高效、安全的實現單例的方式。
5.7、使用容器
/**
* Created by w1992wishes on 2017/11/2.
*/
public class SingletonManager {
private static Map<String, Object> cache = new HashMap<String, Object>();
private SingletonManager(){}
public static void registerInstance(String key, Object instance){
if (!cache.containsKey(key)){
cache.put(key, instance);
}
}
public static Object getInstance(String key){
return cache.get(key);
}
}
這種是用SingletonManager 將多種單例類統一管理,在使用時根據key獲取對象對應類型的對象。
缺點是:這也不是線程安全的,當然可以用ConcurrentHashMap改進。
優點是:這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使用成本,也對用戶隱藏了具體實現,降低了耦合度。
六、優點和缺點
6.1、優點
- 單例模式提供了對唯一實例的受控訪問。因為單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
- 由于在系統內存中只存在一個對象,因此可以節約系統資源,對于一些需要頻繁創建和銷毀的對象單例模式無疑可以提高系統的性能。
- 允許可變數目的實例。基于單例模式我們可以進行擴展,使用與單例控制相似的方法來獲得指定個數的對象實例,既節省系統資源,又解決了單例單例對象共享過多有損性能的問題。
6.2、缺點
- 由于單例模式中沒有抽象層,因此單例類的擴展有很大的困難。
- 單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的創建和產品的本身的功能融合到一起。
- 濫用單例將帶來一些負面問題,如為了節省資源將數據庫連接池對象設計為單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出。
- 現在很多面向對象語言(如Java、C#)的運行環境都提供了自動垃圾回收的技術,因此,如果實例化的共享對象長時間不被利用,系統會認為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實例化,這將導致共享的單例對象狀態的丟失。
七、適用環境
在以下情況下可以使用單例模式:
- 系統只需要一個實例對象,如系統要求提供一個唯一的序列號生成器,或者需要考慮資源消耗太大而只允許創建一個對象。
- 客戶調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。
在一個系統中要求一個類只有一個實例時才應當使用單例模式。反過來,如果一個類可以有幾個實例共存,就需要對單例模式進行改進,使之成為多例模式。
八、模式應用
一個具有自動編號主鍵的表可以有多個用戶同時使用,但數據庫中只能有一個地方分配下一個主鍵編號,否則會出現主鍵重復,因此該主鍵編號生成器必須具備唯一性,可以通過單例模式來實現。
九、總結
- 單例模式確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。單例模式的要點有三個:一是某個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。單例模式是一種對象創建型模式。
- 單例模式只包含一個單例角色:在單例類的內部實現只生成一個實例,同時它提供一個靜態的工廠方法,讓客戶可以使用它的唯一實例;為了防止在外部對其實例化,將其構造函數設計為私有。
- 單例模式的目的是保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。單例類擁有一個私有構造函數,確保用戶無法通過new關鍵字直接實例化它。除此之外,該模式中包含一個靜態私有成員變量與靜態公有的工廠方法。該工廠方法負責檢驗實例的存在性并實例化自己,然后存儲在靜態成員變量中,以確保只有一個實例被創建。
- 單例模式的主要優點在于提供了對唯一實例的受控訪問并可以節約系統資源;其主要缺點在于因為缺少抽象層而難以擴展,且單例類職責過重。
- 單例模式適用情況包括:系統只需要一個實例對象;客戶調用類的單個實例只允許使用一個公共訪問點。