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