单例模式(Singleton-Pattern)百媚生
1 動機
對于系統中的某些類來說,只有一個實例很重要,例如,一個系統中可以存在多個打印任務,但是只能有一個正在工作的任務;一個系統只能有一個窗口管理器或文件系統;一個系統只能有一個計時工具或ID(序號)生成器。
如何保證一個類只有一個實例并且這個實例易于被訪問呢?定義一個全局變量可以確保對象隨時都可以被訪問,但不能防止我們實例化多個對象。
一個更好的解決辦法是讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例被創建,并且它可以提供一個訪問該實例的方法。這就是單例模式的模式動機。
#2 定義
單例模式確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。
單例模式的要點有三個
某個類只能有一個實例
它必須自行創建這個實例
它必須自行向整個系統提供這個實例。單例模式是一種對象創建型模式。單例模式又名單件模式或單態模式。
優點: 1、在內存里只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例(比如管理學院首頁頁面緩存)。 2、避免對資源的多重占用(比如寫文件操作)。
**缺點:**沒有接口,不能繼承,與單一職責原則沖突,一個類應該只關心內部邏輯,而不關心外面怎么樣來實例化
使用場景:
1、要求生產唯一序列號。
2、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來。
3、創建的一個對象需要消耗的資源過多,比如 I/O 與數據庫的連接等。
注意事項: getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化。
結構
分析
單例模式的目的是保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。單例模式包含的角色只有一個,就是單例類——Singleton。單例類擁有一個私有構造函數,確保用戶無法通過new關鍵字直接實例化它。除此之外,該模式中包含一個靜態私有成員變量與靜態公有的工廠方法,該工廠方法負責檢驗實例的存在性并實例化自己,然后存儲在靜態成員變量中,以確保只有一個實例被創建。
在單例模式的實現過程中,需要注意如下三點:
單例類的構造函數為私有;
提供一個自身的靜態私有成員變量;
提供一個公有的靜態工廠方法。
優點
提供了對唯一實例的受控訪問。因為單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它,并為設計及開發團隊提供了共享的概念
由于在系統內存中只存在一個對象,因此可以節約系統資源,對于一些需要頻繁創建和銷毀的對象,單例模式無疑可以提高系統的性能。
允許可變數目的實例。我們可以基于單例模式進行擴展,使用與單例控制相似的方法來獲得指定個數的對象實例。
缺點
由于單例模式中沒有抽象層,因此單例類的擴展困難
單例類的職責過重,在一定程度上違背了“單一職責原則”。
因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的創建和產品的本身的功能融合到一起。
濫用單例將帶來一些負面問題,如
為了節省資源將數據庫連接池對象設計為單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出
現在很多面向對象語言的運行環境都提供了自動垃圾回收的技術,因此,如果實例化的對象長時間不被利用,系統會認為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實例化,這將導致對象狀態的丟失。
#適用場景
系統只需要一個實例對象,如
系統要求提供一個唯一的序列號生成器
需要考慮資源消耗太大而只允許創建一個對象
客戶調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。
在一個系統中要求一個類只有一個實例時才應當使用單例模式。反過來,如果一個類可以有幾個實例共存,就需要對單例模式進行改進,使之成為多例模式
應用
一個具有自動編號主鍵的表可以有多個用戶同時使用,但數據庫中只能有一個地方分配下一個主鍵編號,否則會出現主鍵重復,因此該主鍵編號生成器必須具備唯一性,可以通過單例模式來實現。
總結
單例模式確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例,這個類稱為單例類,它提供全局訪問的方法。單例模式的要點有三個:一是某個類只能有一個實例;二是它必須自行創建這個實例;三是它必須自行向整個系統提供這個實例。單例模式是一種對象創建型模式。
單例模式只包含一個單例角色:在單例類的內部實現只生成一個實例,同時它提供一個靜態的工廠方法,讓客戶可以使用它的唯一實例;為了防止在外部對其實例化,將其構造函數設計為私有。
單例模式的目的是保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。單例類擁有一個私有構造函數,確保用戶無法通過new關鍵字直接實例化它。除此之外,該模式中包含一個靜態私有成員變量與靜態公有的工廠方法。該工廠方法負責檢驗實例的存在性并實例化自己,然后存儲在靜態成員變量中,以確保只有一個實例被創建。
單例模式的主要優點在于提供了對唯一實例的受控訪問并可以節約系統資源;其主要缺點在于因為缺少抽象層而難以擴展,且單例類職責過重。
單例模式適用情況包括:系統只需要一個實例對象;客戶調用類的單個實例只允許使用一個公共訪問點。
實現方式
1、懶漢式(非線程安全)
最基本的實現方式
不支持多線程。因為沒有加鎖 synchronized,所以嚴格意義上并不算單例模式
這種方式 lazy loading 很明顯,不要求線程安全,當有多個線程并行調用 getInstance() 的時候,就會創建多個實例。也就是說在多線程下不能正常工作。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2、懶漢式(線程安全)
為了解決上面的問題,最簡單的方法是將整個 getInstance() 方法設為同步(synchronized)
優點:第一次調用才初始化,避免內存浪費。
缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
雖然做到了線程安全,并解決了多實例的問題,但并不高效。
因為在任何時候只能有一個線程調用 getInstance()
但是同步操作只需要在第一次調用時才被需要,即第一次創建單例實例對象時。
這就引出了雙重檢驗鎖。
public class Singleton {
private static volatile Singleton INSTANCE = null;
// Private constructor suppresses
// default public constructor
private Singleton() {}
//thread safe and performance promote
public static Singleton getInstance() {
if(INSTANCE == null){
synchronized(Singleton.class){
//when more than two threads run into the first null check same time, to avoid instanced more than one time, it needs to be checked again.
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
##3 餓漢式
較常用,但易產生垃圾對象
優點:無鎖,執行效率提高
缺點:類加載時就初始化,浪費內存
非常簡單,實例被聲明成 static 和 final變量了,在第一次加載類到內存中時就會初始化,所以創建實例本身是線程安全的。
它基于類加載機制避免了多線程的同步問題
不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用 getInstance, 但也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance 顯然沒有達到lazy loading
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
// Private constructor suppresses
private Singleton() {}
// default public constructor
public static Singleton getInstance() {
return INSTANCE;
}
}
這種寫法如果完美的話,就沒必要在啰嗦那么多雙檢鎖的問題了。
缺點是它不是一種懶加載模式(lazy initialization),單例會在加載類后一開始就被初始化,即使客戶端沒有調用 getInstance()方法。
餓漢式的創建方式在一些場景中將無法使用:譬如 Singleton 實例的創建是依賴參數或者配置文件的,在 getInstance() 之前必須調用某個方法設置參數給它,那樣這種單例寫法就無法使用了。
4、雙重檢驗鎖模式(double checked locking pattern)
一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查instance == null
一次是在同步塊外
一次是在同步塊內。為什么在同步塊內還要再檢驗一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個實例了。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) { //Single Checked
synchronized (Singleton.class) {
if (singleton == null) { //Double Checked
singleton = new Singleton();
}
}
}
return singleton;
}
}
看起來很完美,很可惜哦,它還是有問題。
主要在于
instance = new Singleton()
并非一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情
1、memory = allocate() 分配對象的內存空間
2、ctorInstance() 調用 Singleton 的構造函數來初始化成員變量
3、instance = memory 設置instance指向剛分配的內存執行完這步 instance 就為非 null 了)
JVM和CPU優化,發生了指令重排
但是在 JVM 的JIT 中存在指令重排序的優化。
也就是說上面的第2步和第3步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是后者
1、memory = allocate() 分配對象的內存空間
3、instance = memory 設置instance指向剛分配的內存
2、ctorInstance() 初始化對象
則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
只需要將 instance 變量聲明成volatile
public class Singleton {
private volatile static Singleton instance; //聲明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
有些人認為使用 volatile 的原因是可見性,也就是可以保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。
但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。
在 volatile 變量的賦值操作后面會有一個內存屏障(生成的匯編代碼上),讀操作不會被重排序到內存屏障之前。
比如上面的例子,取操作必須在執行完 1-2-3 之后或者 1-3-2 之后,不存在執行到 1-3 然后取到值的情況。從「先行發生原則」的角度理解的話,就是對于一個 volatile 變量的寫操作都先行發生于后面對這個變量的讀操作(這里的“后面”是時間上的先后順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,所以在這之后才可以放心使用 volatile。
相信你不會喜歡這種復雜又隱含問題的方式,當然我們有更好的實現線程安全的單例模式的辦法。
#5、靜態內部類
線程安全
實現難度: 一般
描述: 這種方式能達到雙檢鎖方式一樣的功效,但實現更簡單
對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式
這種方式只適用于靜態域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用。
這種方式同樣利用了 classloder 機制來保證初始化 instance 時只有一個線程,它跟第 3 種方式不同的是:第 3 種方式只要 Singleton 類被裝載了,那么 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用,只有通過顯式調用 getInstance 方法時,才會顯式裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,所以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時就實例化,因為不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載,那么這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比第 3 種方式就顯得很合理
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 枚舉
JDK5 起
線程安全
實現單例模式的最佳方法。
它更簡潔,自動支持序列化機制,絕對防止多次實例化。
Effective Java 作者 Josh Bloch 提倡的方式,它不僅能
避免多線程同步問題
自動支持序列化機制
防止反序列化重新創建新的對象
絕對防止多次實例化
不能通過 reflection attack 來調用私有構造方法。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
經驗
一般情況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 3 種餓漢方式。
只有在要明確實現 lazy loading 效果時,才會使用第 5 種登記方式。
如果涉及到反序列化創建對象時,可以嘗試使用第 6 種枚舉方式。
如果有其他特殊的需求,可以考慮使用第 4 種雙檢鎖方式。
?
轉載于:https://juejin.im/post/5c3465a36fb9a049a81f7fac
總結
以上是生活随笔為你收集整理的单例模式(Singleton-Pattern)百媚生的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++服务器开发的必备利器–libc
- 下一篇: Ceph分布式存储高性能设计