天天看點

【設計模式學習筆記】3:單例模式的實作(懶漢式,餓漢式,DCL,登記式,靜态内部類,枚舉)

簡述

單例模式表示在記憶體中隻有一個執行個體,多次使用該類的對象時,使用的都是同一個對象。單例模式可以避免一個全局使用的類被頻繁地建立和銷毀。

單例模式需要将構造函數私有化(避免外部使用構造函數建立對象),并為單例對象提供一個全局的通路點。

幾種實作方式

以下隻考慮線程安全的實作方式,線程不安全的不被認為是單例模式。

[1]懶漢式

懶漢式即指在第一次使用時才建立對象。

public class SingletonClass {
    //組合一個自己的對象引用,private保護其不被直接通路修改,static保護本類唯一
    private static SingletonClass singletonClass;

    //構造函數私有化,使不能從外部建立本類對象
    private SingletonClass() {
    }

    //獲得單例執行個體的靜态方法作外部通路點
    //用synchronized修飾為同步,才能確定多線程環境下隻建立這一個對象
    public static synchronized SingletonClass getInstance() {
        return null == singletonClass ? singletonClass = new SingletonClass() : singletonClass;
    }
}
           

在這種方式下,因為将外部通路點整個設定為同步的,是以多線程環境下工作效率很低。當

getInstance()

操作的同步對整個系統的性能不是很關鍵時,如需要避免餓漢建立該對象造成的記憶體浪費,不妨使用這種方式。

[2]餓漢式

餓漢式是在類加載時才建立這個對象,但應注意餓漢式不一定懶加載。可以将懶加載視為在第一次調用

getInstance()

方法時建立這個對象,懶漢式是能保證懶加載的,餓漢式不能。

public class SingletonClass {
    //在類加載時建立對象,餓漢式
    private static SingletonClass singletonClass=new SingletonClass();

    //構造函數私有化,使不能從外部建立本類對象
    private SingletonClass() {
    }

    //調用到此方法時類一定已經加載過了,直接傳回
    //不需要synchronized同步
    public static SingletonClass getInstance() {
        return singletonClass;
    }
}
           

在這種方式中,巧妙利用了Java的類裝載過程來保證了線程安全,因為這個類隻加載一次,是以這個對象一定是唯一的。并且因為沒有

synchronized

對通路點的限制,這種方式的通路效率比較高。

除了要注意記憶體浪費之外,還應注意到“類裝載時”不一定是”第一次調用通路點時”,因為類中還可能存在其它的static方法在此前調用導緻對象被建立,是以餓漢式不能保證懶加載,可能早在調用其它靜态方法時就把這個對象建立好了。

[3]雙檢鎖方式

使用雙重校驗鎖(Double Checked Locking),可以結合懶漢式和餓漢式的優點。既不浪費記憶體(做到懶加載),又不至于讓外部通路點性能下降太多。

public class SingletonClass {
    //volatile保證有線程對該變量修改時,另一個線程中該變量的緩存行無效,讀取時直接到記憶體讀
    //總之,若一個線程修改了某個變量的值,新值對其他線程來說是立即可見的,在通路點内檢查時要用到
    private volatile static SingletonClass singletonClass;

    //構造函數私有化,使不能從外部建立本類對象
    private SingletonClass() {
    }

    //使用DCL鎖保證線程安全,不需要對整個方法synchronized同步
    public static SingletonClass getInstance() {
        //如果該線程發現該對象未建立
        if (null == singletonClass) {
            //那麼首先要和其它線程競争本類的鎖
            synchronized (SingletonClass.class) {
                //獲得鎖以後,才能執行這部分代碼
                //這時再次檢查是否為null
                //如果還是null,說明自己是第一個競争到鎖的,本線程負責建立對象
                if (null == singletonClass)
                    singletonClass = new SingletonClass();
                //如果不是null了,說明自己這份鎖已經是别人用過,建立好對象以後釋放出來的
                //這時對象已經被建立過了,本線程什麼都不用做,直接釋放鎖即可
            }
        }
        //至此,對象一定唯一地建立過了,直接傳回
        return singletonClass;
    }
}
           

這種方式最複雜,性能損失較小但也并非最好,其優勢在于在建立對象時能做的事情非常多。

[4]登記式

登記式是用一個線程安全的容器(網上很多登記式都用

HashMap

,這是線程不安全的,用

ConcurrentHashMap

才是正确的選擇)來對要單例化的執行個體進行登記,當使用時直接從這個容器中取出即可。

可以單獨設定一個類來管理要登記的單例對象,也可以為單例對象類自己設定登記容器(見線程安全的登記式單例)。下面示範一下前者,即設定一個單獨的類來管理登記。

//單例管理類
public class SingletonManager {
    //線程安全的容器,餓漢式保證容器對象本身為單例
    private static Map map = new ConcurrentHashMap();

    //外部通路點,傳入類名,傳回該類的單例對象.該類會被登記進入上面的容器進行單例管理
    //在類中務必保證構造方法私有化,對這一點這個管理類是無法控制的,需要自己保證
    public static Object getInstance(String className) {
        //如果還沒登記到容器
        if (!map.containsKey(className)) {
            //用反射的方式建立對象(因為已經構造函數私有化),并登記到容器中
            try {
                map.put(className, Class.forName(className).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        //從容器中擷取管理的單例對象并傳回
        return map.get(className);
    }
}
           

可以看到,在外部管理類登記是需要用反射來建立對象的,因為構造方法已經私有化了,而在内部登記自己則不用。

[5]靜态内部類

即使用靜态内部類來實作,這種方式是對餓漢式的改進。既然餓漢式因為在類加載時就會建立對象,進而導緻不能保證懶加載,那麼不妨增加一個靜态内部類,并且隻在通路點方法中調用這個靜态内部類,如此在這個靜态内部類加載時建立對象就不會受到其它靜态方法的影響了。

public class SingletonClass {
    //私有的靜态内部類,隻有這個類内才能通路
    private static class SingletonHolde {
        //該對象不會在外部類加載時便建立,避免受外部類其它靜态方法影響
        private static SingletonClass singletonClass = new SingletonClass();
    }

    //構造函數私有化,使不能從外部建立本類對象
    private SingletonClass() {
    }

    //外部通路點
    public static SingletonClass getInstance() {
        //第一次引用内部類時才會加載建立這個對象,而隻要保證此必在這方法内,就做到了懶加載
        return SingletonHolde.singletonClass;
    }
}
           

一般隻有在明确要求懶加載時,才使用這種方式。

[6]枚舉

枚舉的構造函數本身就是私有的,而且可以自由序列化、線程安全、保證單例。使用枚舉是實作單例模式的最佳方式。以前對枚舉不太了解,實際上枚舉就是一個final類,也一樣可以有其它屬性和方法,當成普通類來用實作單例極為簡便。

public enum SingletonEnum {
    INSTANCE;//枚舉對象天然就是單例

    //枚舉類也一樣可以有其它屬性
    private int id = ;

    //枚舉類也一樣可以有其它方法
    public void sayId() {
        System.out.println("id是" + id);
    }
}

//測試一下
class Main {
    public static void main(String[] args) {
        SingletonEnum.INSTANCE.sayId();
        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}
           

輸出:

id是
true
           

參考閱讀

[1] Java并發程式設計:volatile關鍵字解析

[2] 單例模式以及雙檢鎖DCL對DCL講的比較清楚。

[3] Java枚舉enum以及應用:枚舉實作單例模式

[4] 單例模式-runoob

[5] 深入了解Java枚舉類型(enum)寫得極好,有空好好學習一下。

繼續閱讀