23種設計模式之《單例模式》


什麼是單例模式

單例模式是23種設計模式中最簡單和易用的模式。在某些情境下,如在一個上市公司中,有很多不同級別的員工,但是公司的CEO或者CTO都是隻有一個的,CEO或者CTO在公司裏就要求是一個單例。單例模式,就是某個類因實際情況的需要,要求在全局的範圍內只能有唯一的實例對象,這個對象是常駐內存的,可以重複使用,降低重複創建對象的開銷。

單例模式的特點

  • 類的構造函數是私有的
  • 在類內部實例化對象,並通過靜態方法向外提供實例化的對象

下面主要講解實現單例模式的方法以及它們的優缺點

單例模式的實現

單例模式的目的,就是要確保在全局範圍內某個類的對象是唯一的。所以實現單例模式時,我們至少要考慮兩個影響對象創建的因素。

  • 在併發的環境下的線程安全
  • 反序列化

餓漢實現

在類第一次加載時,就進行對象的實例化。

public class SingletonDemo {

    private final static SingletonDemo mSingletonDemo = new SingletonDemo();

    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        return mSingletonDemo;
    }

}

懶漢實現

在類加載時不進行對象的實例化,只在對象被第一次訪問時,才進行對象的實例化。

public class SingletonDemo {

    private static SingletonDemo mSingletonDemo;

    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        if(mSingletonDemo == null) {
            mSingletonDemo = new SingletonDemo();
        }
        return mSingletonDemo;
    }

}

明顯,在多線程的環境下,上面兩種實現方式都不是線程安全的。爲了實現線程安全,我們首先可以想到使用synchronized關鍵字。

線程安全的懶漢模式

public class SingletonDemo {

    private static SingletonDemo mSingletonDemo;

    private SingletonDemo() {}

    public static synchronized SingletonDemo getInstance() {

        if(mSingletonDemo == null) {
            mSingletonDemo = new SingletonDemo();
        }
        return mSingletonDemo;
    }

}

關於synchronized關鍵字說明一下,synchronized聲明的靜態方法,同時只能被一個執行線程訪問,但是其他線程可以訪問這個對象的非靜態方法。即:兩個線程可以同時訪問一個對象的兩個不同的synchronized方法,其中一個是靜態方法,一個是非靜態方法。

所以,當有多個線程同時訪問getInstance靜態方法時,多個其他的線程只能等待,這時只有一個線程能夠訪問getInstance方法,等這個線程釋放後其他線程才能訪問。這樣就會影響速度和效率。

爲了提高懶漢模式的速度和效率,可以減小鎖的粒度和次數。

雙重校驗鎖法

public class SingletonDemo {

    private static SingletonDemo mSingletonDemo;

    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        if(mSingletonDemo == null) {
            synchronized (SingletonDemo.class) {
                if(mSingletonDemo == null) {
                    mSingletonDemo = new SingletonDemo();
                }
            }
        }
        return mSingletonDemo;
    }

}

從上面可以看到,只有在第一次訪問時纔會鎖定和創建類的對象,之後的訪問都是直接使用已經創建好的對象,這樣減少鎖定的次數和範圍,以達到提高單例模式的效率。

但是,對象的實例化,並不是一個原子性操作。即第11行代碼處,它可以分成下面三個步驟:
1、new SingletonDemo(),爲SingletonDemo實例分配內存
2、調用SingletonDemo的構造器,完成初始化工作
3、將mSingletonDemo指向分配的內存空間

由於java處理器可以亂序執行,即無法保證2和3的執行順序。這對雙重校驗鎖法實現的單例模式有什麼影響呢?
當第一個線程訪問getInstance方法時,會鎖定臨界區(第9行到第13行代碼),它實例化對象的順序是1=>3=>2,而在這時如果有第二個線程來訪問getInstance方法,由於第一個線程在處理器中執行完了3未執行2,第二個線程會馬上得到實例對象,因爲第一個線程的3已經執行完即mSingletonDemo已經不爲空。當第二個線程使用沒有初始化的對象時就會出現問題。

所以,雙重校驗鎖法也不是完美的,在併發環境下依然可能出現問題。

靜態內部類實現

public class SingletonDemo {

    private static SingletonDemo mSingletonDemo;

    private SingletonDemo() {}

    private static class SingletonHolder {
        private static final SingletonDemo INSTANCE = new SingletonDemo();
    }

    public static SingletonDemo getInstance() {
        return SingletonHolder.INSTANCE;
    }

}

第一次加載SingletonDemo類時並不會實例化INSTANCE,只有在第一次調用getInstance方法時,纔會加載SingletonHolder內部類,創建SingletonDemo實例。這種方式不僅確保了線程安全,也保證單例對象的唯一性,同時也實現了單例對象的懶加載。

枚舉實現

上面幾種實現方式,可能會因爲反序列化而創建新的實例,所以必須重寫readResolve方法,在readResolve方法中返回已經創建的單例。

使用枚舉可以很簡單的實現單例模式,這也是Effective Java中提倡的方式。因爲枚舉本身就是類型安全的,並且枚舉實例在任何情況下都是單例。

public enum SingletonEnumDemo {
    INSTANCE;
    public void justDoYourThing() {

    }
}

枚舉單例使用

SingletonEnumDemo.INSTANCE.justDoYourThing();

容器實現

public class SingletonDemo {

    private static Map<String, Object> singletonMap = new HashMap<String, Object>();

    private SingletonDemo() {}

    public static void registerService(String key, Object instance)     {
        if (!singletonMap.containsKey(key)) {
            singletonMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return singletonMap.get(key);
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章