小小的單例模式竟然有這么多種寫法?
單例模式應(yīng)該是設(shè)計模式中最容易理解也是用得最多的一種模式了,同時也是面試的時候最常被問到的模式。
1. 單例模式的定義
單例模式指的是一個類中在任何情況下都絕對只有一個實例,并且提供一個全局訪問點。
2. 單例模式的應(yīng)用場景
單例模式的應(yīng)用非常廣泛,如數(shù)據(jù)庫中的連接池、J2EE中的ServletContext和ServletContextConfig、Spring框架中的ApplicationContext等等。然而在Java中,單例模式還可以保證一個JVM中只存在一個唯一的實例。
單例模式的應(yīng)用場景主要有以下幾個方面:
- 當(dāng)需要頻繁創(chuàng)建一些類的時候,使用單例可以降低系統(tǒng)的內(nèi)存壓力,減少GC(垃圾回收) ;
- 當(dāng)某些類創(chuàng)建實例時候需要占用的資源較多,或者實例化過程耗時比較長,且經(jīng)常使用的情況;
- 當(dāng)存在頻繁訪問數(shù)據(jù)庫或者文件的對象;
- 當(dāng)對于一些控制硬件級別的操作,或者從系統(tǒng)上來講應(yīng)當(dāng)是單一控制邏輯的操作,是不允許存在多個實例的,否則玩完;
3. 單例模式的優(yōu)缺點
3.1 單例模式的優(yōu)點
- 單例模式可以保證內(nèi)存中只有一個實例對象,從而會減少內(nèi)存的開銷;
- 單例模式可以避免對資源的多重占用;
- 單例模式設(shè)置全局訪問點,可以起到優(yōu)化和共享資源的訪問的作用;
3.2 單例模式的缺點
- 擴展難, 因為單例模式通常是沒有接口的啊,如果想要擴展,那么你唯一途徑就是修改之前的代碼,所以說單例模式違背了開閉原則;
- 調(diào)試難,因為在并發(fā)測試中,單例模式是不利于代碼的調(diào)試的,單例中的代碼沒有執(zhí)行完,也不能模擬生成一個新對象;
- 違背單一職責(zé)原則,因為單例模式的業(yè)務(wù)代碼通常寫在一個類中,如果功能設(shè)計不合理,就很容易違背單一職責(zé)原則;
4. 單例模式的實現(xiàn)方式及其優(yōu)缺點
4.1 單例模式的餓漢式實現(xiàn)
4.1.1 餓漢式標(biāo)準(zhǔn)寫法
Singleton類稱為單例類,通過內(nèi)部初始化一次 , 隱藏構(gòu)造方法, 并提供一個全局訪問點的方式實現(xiàn)。
- /**
- * msJava
- *
- * @Description 單例模式的通用寫法
- * @Date 2021-01-23
- */
- public class Singleton {
- /**
- * 內(nèi)部初始化一次
- */
- private static final Singleton instance = new Singleton();
- /**
- * 隱藏構(gòu)造方法
- */
- private Singleton() {
- }
- /**
- * 提供一個全局訪問點
- *
- * @return Singleton
- */
- public static Singleton getInstance() {
- return instance;
- }
- }
以上餓漢式單例寫法在類的初始化的時候就會進行初始化操作,并且創(chuàng)建對象,絕對的線程安全,因為此時線程還沒有出現(xiàn)就已經(jīng)實例化了,故不會存在訪問安全的問題。
4.1.2 餓漢式靜態(tài)塊機制寫法
餓漢式還有一種實現(xiàn),那就是靜態(tài)塊機制,如下代碼所示:
- /**
- * msJava
- *
- * @Description 單例模式 餓漢式靜態(tài)機制 實現(xiàn)
- * @Date 2021-01-23
- */
- public class HungryStaticSingleton {
- private static final HungryStaticSingleton hungrySingleton;
- //靜態(tài)代碼塊 類加載的時候就初始化
- static {
- hungrySingleton=new HungryStaticSingleton();
- }
- /**
- * 私有化構(gòu)造函數(shù)
- */
- private HungryStaticSingleton(){}
- /**
- * 提供一個全局訪問點
- * @return
- */
- public static HungryStaticSingleton getInstance() {
- return hungrySingleton;
- }
- }
我們分析一下這種是寫法 ,可以明顯的看到所以對象是類在加載的時候就進行實例化了,那么這樣一來,會導(dǎo)致單例對象的數(shù)量不確定,從而會導(dǎo)致系統(tǒng)初始化的時候就造成大量內(nèi)存浪費,況且你用不用還不一定,還一直占著空間,俗稱“占著茅坑不拉屎”。
4.2 單例模式的懶漢式實現(xiàn)
為了解決餓漢式單例寫法可能帶來的內(nèi)存浪費問題,這里分析一下懶漢式單例的寫法。如下代碼所示:
- /**
- * msJava
- *
- * @Description 單例模式 懶漢式單例實現(xiàn)
- * @Date 2021-01-23
- */
- public class LazySimpleSingleton {
- private static LazySimpleSingleton lazySingleton = null;
- /**
- * 私有化構(gòu)造函數(shù)
- */
- private LazySimpleSingleton() {
- }
- /**
- * 提供一個全局訪問點
- *
- * @return
- */
- public static LazySimpleSingleton getInstance() {
- if (lazySingleton == null) {
- lazySingleton = new LazySimpleSingleton();
- }
- return lazySingleton;
- }
- }
這樣實現(xiàn)的好處就是只有對象被使用的時候才會進行初始化,不會存在內(nèi)存浪費的問題,但是它會在多線程環(huán)境下,存在線程安全問題。我們可以利用synchronized關(guān)鍵字將全局訪問點方法變成一個同步方法,這樣就可以解決線程安全的問題,代碼如下所示:
- /**
- * msJava
- *
- * @Description 單例模式 懶漢式單例實現(xiàn) synchronized修飾
- * @Date 2021-01-23
- */
- public class LazySimpleSingleton {
- private static LazySimpleSingleton lazySingleton = null;
- /**
- * 私有化構(gòu)造函數(shù)
- */
- private LazySimpleSingleton() {}
- /**
- * 提供一個全局訪問點
- *
- * @return
- */
- public synchronized static LazySimpleSingleton getInstance() {
- if (lazySingleton == null) {
- lazySingleton = new LazySimpleSingleton();
- }
- return lazySingleton;
- }
- }
但是,這樣雖然解決了線程安全的問題,可是如果在線程數(shù)量劇增的情況下,用synchronized加鎖,則會導(dǎo)致大批線程阻塞,從而驟減系統(tǒng)性能。
4.3 單例模式的雙重檢測實現(xiàn)
在上述代碼上進一步優(yōu)化,代碼如下所示:
- /**
- * msJava
- *
- * @Description 單例模式 懶漢式-雙重檢測單例實現(xiàn)
- * @Date 2021-01-23
- */
- public class LazyDoubleCheckSingleton {
- // volatile 關(guān)鍵字修飾
- private volatile static LazyDoubleCheckSingleton lazySingleton ;
- /**
- * 私有化構(gòu)造函數(shù)
- */
- private LazyDoubleCheckSingleton() {}
- /**
- * 提供一個全局訪問點
- *
- * @return
- */
- public static LazyDoubleCheckSingleton getInstance() {
- // 這里先判斷一下是否阻塞
- if (lazySingleton == null) {
- synchronized (LazyDoubleCheckSingleton.class){
- // 判斷是否需要重新創(chuàng)建實例
- if (lazySingleton == null) {
- lazySingleton = new LazyDoubleCheckSingleton();
- }
- }
- }
- return lazySingleton;
- }
- }
()方法時,第二個線程也可以調(diào)用,但是第一個線程執(zhí)行synchronized時候,第二個線程就會發(fā)現(xiàn)阻塞,但是此時的阻塞是getInstance()內(nèi)部的阻塞。
4.4 單例模式的靜態(tài)內(nèi)部類實現(xiàn)
雖然雙重檢測鎖的單例模式解決了線程安全和性能問題,但是畢竟涉及加鎖的操作,多多少少就會到了性能的影響,下面我們分享一下更加優(yōu)雅的單例模式實現(xiàn),如下代碼所示:
- /**
- * msJava
- *
- * @Description 單例模式 靜態(tài)內(nèi)部類單例實現(xiàn)
- * @Date 2021-01-23
- */
- public class LazyStaticInnerClassSingleton {
- // 在構(gòu)造方法里面拋出異常真的合適?
- private LazyStaticInnerClassSingleton(){
- if(LazyHolder.INSTANCE != null){
- throw new RuntimeException("不允許創(chuàng)建多個實例");
- }
- }
- // static 保證這個方法不會被重寫 覆蓋
- private static LazyStaticInnerClassSingleton getInstance(){
- return LazyHolder.INSTANCE;
- }
- // Java 默認不會加載內(nèi)部類
- private static class LazyHolder{
- private static final LazyStaticInnerClassSingleton INSTANCE=new LazyStaticInnerClassSingleton();
- }
- }
5. 總結(jié)
單例模式面試幾乎必備!