單例模式 Singleton
單例就是單一實例, only you 只有一個
意圖
保證一個類僅有一個實例,而且提供一個訪問他的全局訪問點
單例模式的含義簡單至極,複雜的是如何可以保障你真的只是建立了一個實例
怎樣才能保證一個類只有一個實例,而且這個實例對象還易於被訪問?
能夠藉助於全局變量,可是類就在那裏,你不能防止實例化多個對象,可能一不當心誰就建立了一個對象
因此一般的作法是讓類自身負責保存他的惟一實例,經過構造方法私有阻止外部實例對象,而且提供靜態公共方法
因此常說的單例模式有下面三個特色
- 單例模式的類,只能有一個實例對象
- 單例模式的類,自身建立本身惟一的實例對象
- 單例模式的類,必須提供獲取這一惟一實例的方式
結構
Singleton模式的結構簡單,實現的步驟通常是:
自身建立而且保存維護這個惟一實例,而且這個惟一實例singleton 是私有的
將構造方法設置爲私有,防止建立實例
設置公共的getInstance()方法獲取實例,並且,這個方法必然是靜態的
單例類自身負責建立維護惟一實例,按照實例對象建立的時機,分爲兩類
- 餓漢式:實例在類加載時建立
- 懶漢式:實例在第一次使用時建立
餓漢式
package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class EagerSingleton {
private EagerSingleton() {
}
private static final EagerSingleton singleton = new EagerSingleton();
public static EagerSingleton getInstance() {
return singleton;
}
}
當類加載時,靜態成員singleton 會被初始化,對象在此時被建立
餓漢式的缺點很明顯:
若是初始化的太早,可能就會形成資源浪費。
在虛擬機相關的文章中,有介紹過,虛擬機的實現會保證:類加載會確保類和對象的初始化方法在多線程場景下可以正確的同步加鎖
因此,餓漢式沒必要擔憂同步問題
若是對於該對象的使用也是「餓漢式」的,也就是應用程序老是會高頻使用,應該優先考慮這種模式
懶漢式
package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class LazySingleton {
private LazySingleton() {
}
private static LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}
一個簡單的懶漢式實現方式如上
靜態singleton 初始爲null
每次經過getInstance()獲取時,若是爲null,那麼建立一個實例,不然就直接返回已存在的實例singleton
同步問題
上述代碼在單線程下是沒有問題的,可是在多線程場景下,須要同步
假如兩個線程都執行到if (singleton == null) ,都判斷爲空
那麼接下來兩個線程都會建立對象,就沒法保證惟一實例
因此能夠給方法加上synchronized關鍵字,變爲同步方法
public synchronized static LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
若是內部邏輯不像上面這般簡單,能夠根據實際狀況使用同步代碼塊的形式,好比
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
}
}
return singleton;
}
同步的效率問題
多線程併發場景,並非必然出現的,只是在第一次建立實例對象時纔會出現,機率很是小
可是使用同步方法或者同步代碼塊,則會百分百的進行同步
同步就意味着也就是若是多個線程執行到同一地方,其他線程將會等待
這樣雖然能夠防止建立多個實例,可是有明顯的效率問題
既然同步問題是小几率的,那麼就能夠嘗試下降同步的機率
package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class LazySingleton {
private LazySingleton() {
}
private static LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
上面的方式被稱爲 雙重檢查
若是singleton不爲空,那麼直接返回惟一實例,不會進行同步
若是singleton爲空,那麼涉及到對象的建立,此時,纔會須要同步
只會有一個線程進入同步代碼塊
他會校驗是否的確爲null,而後進行實例對象的建立
既解決了同步問題,又沒有嚴重的效率問題
原子操做問題
計算機中不會由於線程調度被打斷的操做,也就是不可分割的操做,被稱做原子操做
能夠理解爲計算機對指令的執行的最小單位
好比 i=1;這就是一個原子操做,要麼1被賦值給變量i,要麼沒有
可是若是是int i = 1;這就不是一個原子操做
他至少須要先建立變量i 而後在進行賦值運算
咱們實例建立語句,就不是一個原子操做
singleton = new LazySingleton();
他可能須要下面三個步驟
- 分配對象須要的內存空間
- 將singleton指向分配的內存空間
- 調用構造函數來初始化對象
計算機爲了提升執行效率,會作的一些優化,在不影響最終結果的狀況下,可能會對一些語句的執行順序進行調整
也就是上面三個步驟的順序是不可以保證惟一的
若是先分配對象須要的內存,而後將singleton指向分配的內存空間,最後調用構造方法初始化的話
假如當singleton指向分配的內存空間後,此時被另外線程搶佔(因爲不是原子操做因此可能被中間搶佔)
線程二此時執行到第一個if (singleton == null)
此時不爲空,那麼不須要等待線程1結束,直接返回singleton了
顯然,此時的singleton都尚未徹底初始化,就被拿出去使用了
根本問題就在於寫操做未結束,就進行了讀操做
能夠給 singleton 的聲明加上volatile關鍵字,來解決這些問題
能夠保障在完成寫操做以前,不會調用讀操做
完整代碼以下
package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class LazySingleton {
private LazySingleton() {
}
private static volatile LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
內部類的懶漢式
上面的這段代碼,能夠在實際項目中直接使用
可是,雙重檢查難免看起來有些囉嗦
還有其餘的實現方式
內部類是延時加載的,也就是說只會在第一次使用時加載
內部類不使用就不加載的特性,很是適合作單例模式
package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
* @author
*/
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
SingletonHolder做爲靜態內部類,內部持有一個Singleton實例,採用「餓漢式」建立加載
不過內部類在使用時纔會被加載
私有的靜態內部類,只有在getInstance被調用的時候,纔會加載
此時纔會建立實例,因此,從總體效果看是懶漢式
不使用不會加載,節省資源開銷,也不須要複雜的代碼邏輯
依靠類的初始化保障線程安全問題,依靠內部類特性實現懶加載
枚舉單例
《Effective Java》中提到過枚舉針對於單例的應用
使用場景
是否只是須要一個實例,是由業務邏輯決定的
有一些對象本質業務邏輯上就只是須要一個
好比線程池,windows的任務管理器,計算機的註冊表管理器等等
計算機中只須要一個任務管理器,不須要也不必分開成多個,一個任務管理器管理全部任務簡單方便高效
若是qq一個任務管理器idea一個任務管理器,你受得了麼
因此說,是否須要單例模式,徹底根據你的業務場景決定
好比,若是當你須要一個全局的實例變量時,單例模式或許就是一種很好的解決方案
總結
因爲單例模式在內存中只有一個實例,減小了內存開支和系統的性能開銷
單例模式與單一職責模式有衝突
承擔了實例的建立和邏輯功能提供兩種職責
單例模式中沒有抽象層,因此單例類的擴展比較困難
單例模式的選用跟業務邏輯息息相關,好比系統只須要一個實例對象時,就能夠考慮使用單例模式
單例模式的重點在於單例的惟一性的保障實現
能夠直接複製上面的代碼使用
單例模式向多個實例的擴展
單例模式的意圖是「保證一個類僅有一個實例,而且提供一個訪問他的全局訪問點」
單例模式的根本邏輯就是限制實例個數,而且個數限制爲1
因此,能夠仍舊限制實例個數,而且將限制個數設置爲大於等於1
這種單例模式的擴展,又被稱之爲多例模式
- 多例模式下能夠建立多個實例
- 多例模式本身建立、管理本身的實例,並向外界提供訪問方式獲取實例
多例模式其實就是單例模式的天然擴展,同單例模式同樣,也確定須要構造方法私有,多例類本身維護等,惟一不一樣就是實例個數擴展爲多
自定義類加載器時的問題
在虛擬機相關的介紹中有詳細介紹了類加載機制與命名空間以及類加載機制的安全性問題
不一樣的類加載器維護了各自的命名空間,他們是相互隔離的
不一樣的類加載器可能會加載同一個類
若是這種事情發生在單例模式上,系統中就可能存在不止一個實例對象
儘管在不一樣的命名空間中是隔離的
可是在整個應用中就是不止一個,因此若是你自定義了類加載器
你就須要當心,你能夠指定一樣的類加載器以免這個問題
若是沒有自定義類加載器則不須要關心這個問題
自定義的類都會使用內置的 應用程序 類加載器進行加載