單例模式 建立型 設計模式(六)
單例模式 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
這種單例模式的擴充套件,又被稱之為多例模式
- 多例模式下可以建立多個例項
- 多例模式自己建立、管理自己的例項,並向外界提供訪問方式獲取例項
多例模式其實就是單例模式的自然擴充套件,同單例模式一樣,也肯定需要構造方法私有,多例類自己維護等,唯一不同就是例項個數擴充套件為多
自定義類載入器時的問題
在虛擬機器相關的介紹中有詳細介紹了類載入機制與名稱空間以及類載入機制的安全性問題
不同的類載入器維護了各自的名稱空間,他們是相互隔離的
不同的類載入器可能會載入同一個類
如果這種事情發生在單例模式上,系統中就可能存在不止一個例項物件
儘管在不同的名稱空間中是隔離的
但是在整個應用中就是不止一個,所以如果你自定義了類載入器
你就需要小心,你可以指定同樣的類載入器以避免這個問題
如果沒有自定義類載入器則不需要關心這個問題
自定義的類都會使用內建的 應用程式 類載入器進行載入