Android設計模式之單例模式 Singleton

單例模式是設計模式中最簡單的一種, 由於它沒有設計模式中各種對象之間的抽象關係, 所以有人不認爲它是一種模式, 而是一種實現技巧. 單例模式就像字面的意思一樣, 創建全局唯一的一個實例, 提供給外部使用. 要達到這幾點要求就要滿足三點: 私有構造函數(防止被別人實例化), 靜態私有自身對象(用來提供唯一實例), 靜態公有的getInstance方法(用來創建和獲取唯一實例對象).

優缺點: 單例只允許建立一個唯一實例, 不需要頻繁創建和銷燬, 可以節省內存加快對象的訪問速度.

但是單例沒有抽象層和接口, 不方便擴展. 單例既提供工廠方法又提供業務方法, 一定程度上違背了單一職責原則

單例的實現

單例的實現有兩種主流方式, 分別是餓漢模式和懶漢模式, 他們在實例化的時機和效率方面各有不同.

餓漢模式

餓漢模式的實現可以參考字面意思. 餓漢, 飢渴的漢子的意思, 要狼吞虎嚥搶着吃. 既然單例要求全局只能有一個實例, 那我們通過靜態屬性直接new出來一個實例. 由於靜態屬性的初始化是在ClassLoader加載類之後發生的, 而同一個ClassLoader加載同一個類在整個生命週期只會發生一次, 可以保證全局就一個對象實例. 不管業務是否需要用這個實例, 只要應用啓動起來了就把唯一實例給創建出來. 所以餓漢模式的特點是空間換時間, 線程安全.

/**
 * Created by jesse on 15-6-28.
 */
public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {}//一定要有私有構造,要不談何單例
    public static Singleton getInstance(){
        return instance;
    }
}

懶漢模式

既然單例有兩種寫法, 那肯定是要補充餓漢模式不合理的地方. 上面分析了餓漢模式的特點是通過空間換時間, 那麼就會引出一個問題. 假設我係統裏面的單例特別多特別大, 並且有些單例還只是一些不常用的業務在使用, 只使用餓漢模式的話, 會在系統一啓動的時候就會狂吃內存降低內存的利用率.

爲了提高內存的利用率並節省內存空間, 就有了懶漢模式的寫法. 懶漢顧名思義就是一個特別懶的漢子, 既然懶那肯定是什麼時候要用單例的實例了再創建單例的對象. 特點是時間換空間, 延時加載.

要用的時候才創建實例對象, 那可不可以用下面代碼的寫法? 在getInstance時判斷對象是否爲空, 如果爲空說明還沒實例過, 纔去創建個對象存在靜態屬性上.

// 懶漢demo 1
public class Singleton {
    private static Singleton instance;
    private Singleton() {} //一定要有私有構造,要不談何單例
    public static Singleton getInstance() {
        if (null == instance) {
            // yield
            instance = new Singleton();
        }
        return instance;
    }
}

答案當然是不行的. 如果是單線程場景, 上面的寫法是OK的. 但是在多線程的情況下:

  1. 假設線程A運行到//yield註釋這裏時讓出CPU時間片.
  2. CPU時間片輪轉到線程B, 線程B調用getInstance並創建完對象後讓出CPU.
  3. 線程A繼續從//yield處運行, 由於上次運行線程A時已經判斷過實例爲空了, 所以會繼續創建對象.
  4. 至此Singleton類被創建了兩個對象, 已經違反了單例的唯一性.

所以懶漢模式是線程不安全的, 就算你的應用是單線程的, 這裏也強烈不推薦上面的寫法, 因爲誰也保證不了你的應用以後會不會使用多線程. 爲了保證線程安全, 懶漢demo 2裏我們給getInstance方法加上鎖.

// 懶漢demo 2
public class Singleton {
    private static Singleton instance;
    private Singleton() {} 
    public static synchronized Singleton getInstance() {
        //給getInstance方法加鎖
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

給getInstance方法加上互斥鎖後就就能滿足我們的基本要求了. 可是這裏有些同學就有額外的要求了: 由於使用鎖在多線程併發訪問時, 會讓一些線程自旋或掛起消耗額外的系統資源, 通常我們會盡量縮小鎖的粒度只鎖必須要鎖的流程, 上面的代碼可不可以繼續優化呢? 可不可以只鎖起來創建對象的部分呢? 我們一起看下面的demo 3.

// 懶漢demo 3
public class Singleton {
    private static Singleton instance;
    private Singleton() {} 
    public static Singleton getInstance() {
        if (null == instance){
            synchronized(Singleton.class) {
                // yield
                instance = new Singleton();
            }
 
        }
        return instance;
    }
}

demo 3只鎖定了創建對象的部分, 這種寫法的問題跟懶漢demo 1有類似的問題:

  1. 假設線程A申請到鎖運行到//yield時讓出CPU時間片.
  2. CPU輪轉到線程B, 線程B調用getInstance. 由於線程A還沒有創建對象, 所以線程B阻塞等鎖. 讓出CPU給線程A.
  3. 線程A繼續從//yield處運行, 創建第一個單例實例. 釋放鎖後讓出CPU.
  4. 線程B得到CPU並申請到鎖後, 創建第二個單例實例.
  5. 這個流程還是會創建多個單例對象.

在第四步, 當線程B重複申請到鎖的時候instance對象已經有值了, 我們只需要在鎖內對單例對象再次判空就可以避免重複得創建對象了. demo 4的代碼就是懶漢模式的雙檢鎖寫法.

// 懶漢demo 4(雙檢鎖)
public class Singleton {
    private static Singleton instance;
    private Singleton() {} 
    public static Singleton getInstance() {
        if (null == instance) {
            synchronized(Singleton.class) {
                if (null == instance)
                    instance = new Singleton();
            }
 
        }
        return instance;
    }
}

這下萬事大吉了吧, 從代碼上挑不出來任何毛病了. 是的, 懶漢demo 4的雙檢鎖寫法基本沒問題了, 但是從JVM的角度來說還是有很小很小概率會出問題. 這裏涉及到對象的創建過程和JVM的指令重排優化.

對象的創建過程大致分爲下面五步

  1. 類加載檢查: ClassLoader沒有加載過class的話要加載class, 並初始化類的靜態成員變量
  2. 內存分配: 給要創建的對象在堆上分配內存, 並初始化零值.
  3. 設置對象頭: 設置mark word相關信息
  4. 執行構造方法: 執行構造方法, 初始化業務數據.
  5. 賦值內存引用: 把對象的內存指給對象的引用.

正常情況下這五步是按順序執行的. 但是JVM爲了提高指令的執行效率, 會對一些非特定的指令進行重新排序. 可能會將第四步和第五步的順序替換過來.這樣就有可能出現下面的情況:

  1. 懶漢demo 4中, 線程A創建對象觸發指令重排. 執行了對象創建過程的1235, 這時instance已經有了對象的引用, 但是對象還沒有執行構造方法去初始化一些業務數據. 讓出CPU給線程B
  2. 線程B執行getInstance方法. 由於instance對象已經有值了, 線程B會拿着未完整初始化的對象去使用, 可能會引起一系列奇怪的問題.

那麼如何解決JVM的指令重排問題呢? 這裏有需要用到java的一個關鍵字volatile, 大家可能都知道這個關鍵字主要是用來做線程間數據可見的, 其實他還有另外一個作用: 禁止指令重排. 使用volatile修飾的對象, JVM會使用內存屏障來禁止對象相關操作的指令重排. 所以我們還需要再給instance對象加上volatile關鍵字修飾. 如下面demo 5, 至此纔是真正的萬事大吉.

// 懶漢demo 5(最終最完美版)
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (null == instance) {
            synchronized(Singleton.class) {
                if (null == instance)
                    instance = new Singleton();
            }
 
        }
        return instance;
    }
}

總結

根據上面的分析可以很清楚的瞭解到: 餓漢模式不用面對對線程併發的問題; 從首次調用速度和響應時間來說餓漢模式要優於懶漢模式; 從資源利用的角度來說懶漢模式要優於餓漢模式; 懶漢模式最好使用雙檢鎖寫法.


轉載請註明出處:https://blog.csdn.net/l2show/article/details/46672061

 
發佈了66 篇原創文章 · 獲贊 15 · 訪問量 78萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章