Android 常用設計模式(二) -- 單例模式(詳解)

夏至的稻穗發表於2017-07-01

作者 : 夏至 歡迎轉載,也請保留這段申明
blog.csdn.net/u011418943/…

上一篇講到策略模式,變動的程式碼需要用到策略模式,感興趣的小夥伴可以看看.
傳送門:Android 常用設計模式之 -- 策略模式

單例模式的定義就不解釋過多了,相信很多小夥伴在設計的時候,都用到這個模式;常用的場景為 資料庫的訪問,檔案流的訪問以及網路連線池的訪問等等,在這些場景中,我們都希望例項只有一個,除了減少記憶體開銷之外,也防止防止多程式修改檔案錯亂和資料庫鎖住的問題。
在這一篇文章中,我將帶你分析 android 常見的集中單例模式,並詳細分析他們的優缺點。讓大家在以後的選擇單例中,可以根據實際情況選擇。
當然,如有錯誤,也歡迎指正。
下面是介紹:

1、餓漢式

就是初始化的時候,直接初始化,典型的以時間換空間的做法。

public class SingleMode{
    //構造方法私有化,這樣外界就不能訪問了
    private SingleMode(){
    };
    //當類被初始化的時候,就直接new出來
    private static SingleMode instance = new SingleMode();
    //提供一個方法,給他人呼叫
    public static SingleMode getInstance(){
        return instance;
    }

}複製程式碼

餓漢式的好處是執行緒安全,因為虛擬機器保證只會裝載一次,再裝載類的時候,是不會併發的,這樣就保證了執行緒安全的問題。
但缺點也很明顯,一初始化就例項佔記憶體了,但我褲子還沒脫,不想用呢。

2、懶漢式

為了解決上面的問題,開了懶漢式,就是需要使用的時候,才去載入;

public class SingleMode{
    //構造方法私有化,這樣外界就不能訪問了
    private SingleMode(){
    };
    private static SingleMode mSingleMode;
    public static SingleModegetInstance(){ //這裡就是延時載入的意思
        if (mSingleMode == null){
            mSingleMode = new SingleMode();
        }
        return  mSingleMode;
    }
}複製程式碼

懶漢式如上所示,
優點:

我們只需要在用到的時候,才申請記憶體,且可以從外部獲取引數再例項化,這點是懶漢式的最大優點了

缺點:

單執行緒只例項了一次,如果是多執行緒了,那麼它會被多次例項

至於問什麼說它是執行緒不安全的呢?先下面這張圖:

我們假設一下,有兩個執行緒,A和B都要初始化這個例項;此時 A 比較快,已經判斷 mSingleMode 為null,正在建立例項,而 B 這時候也再判斷,但此時 A 還沒有 new 完,所以 mSingleMode 還是為空的,所以B 也開始 new 出一個物件出來,這樣就相當於建立了兩個例項了,所以,上面這種設計並不能保證執行緒安全。

2.1、如何實現懶漢式執行緒安全?

有人會說,簡單啊,你既然是執行緒併發不安全,那麼加上一個 synchronized 執行緒鎖不就完事了?但是這樣以來,會降低整個訪問速度,而且每次都要判斷,這個真的是我們想要的嗎?

由於上面的缺點,所以,我們可以對上面的懶漢式加個優化,如雙重檢查枷鎖:

public class SingleMode{
    //構造方法私有化,這樣外界就不能訪問了
    private SingleMode(){
    };
    private static SingleMode mSingleMode;
    public static SingleMode getInstance(){
        if (mSingleMode == null){
           synchronized (SingleMode.class){
               if (mSingleMode == null){  //二次檢測
                   mSingleMode = new SingleMode();
               }
           }
        }
        return  mSingleMode;
    }
}複製程式碼

在上面的基礎上,用了二次檢查,這樣就保證了執行緒安全了,它會先判斷是否為null,是才會去載入,而且用 synchronized 修飾,則又保證了執行緒安全。

但是如果上面我們沒有用 volatile 修飾,它還是不安全的,有可能會出現null的問題。為什麼?這是因為 java 在 new 一個物件的時候,它是無序的。而這個過程我們假設一下,假如有執行緒A,判斷為null了,這個時候它就進入執行緒鎖了 mSingleMode = new SingleMode();,它不是一蹴而就,而是需要3步來完成的。

  • 1、為 mSingleMode 建立記憶體
  • 2、new SingleMode() 呼叫這個構造方法
  • 3、mSingleMode 指向記憶體區域

那你可能會有疑問,這樣不是很正常嗎?怎麼會有 null 的情況?
非也,java 虛擬機器在執行上面這三步的時候,並不是按照這樣的順序來的,可能會打亂,這兒就是java重排序,比如2和3調換一下:

  • 1、為 mSingleMode 建立記憶體
  • 3、mSingleMode 指向記憶體區域
  • 2、new SingleMode() 呼叫這個構造方法

那這個時候,mSingleMode 已經指向記憶體區域了,那這個時候它就不為 null了,而實際上它並未獲得構造方法,比如構造方面裡面有些引數或者方法,但是你並未獲取,然而這個時候執行緒B過來,而 mSingleMode已經指向記憶體區域不為空了,但方法和引數並未獲得, 所以,這樣你執行緒B在執行 mSingleMode 的某些方法時就會報錯。

當然這種情況是非常少見的,不過還是暴露了這種問題所在。
所以我們用volatile 修飾,我們都知道 volatile 的一個重要屬性是可見性,即被 volatile 修飾的物件,在不同執行緒中是可以實時更新的,也是說執行緒A修改了某個被volatile修飾的值,那麼我執行緒B也知道它被修改了。但它還有另一個作用就是禁止java重排序的作用,這樣我們就不用擔心出現上面這種null 的情況了。如下:

public class SingleMode{
    //構造方法私有化,這樣外界就不能訪問了
    private SingleMode(){
    };
    private volatile static SingleMode mSingleMode;
    public static SingleMode getInstance(){
        if (mSingleMode == null){
           synchronized (SingleMode.class){
               if (mSingleMode == null){  //二次檢測
                   mSingleMode = new SingleMode();
               }
           }
        }
        return  mSingleMode;
    }
}複製程式碼

看到這裡,是不是感覺爬了幾百盤的坑,終於上了黃金段位了。。。
然而,並不是,你打了排位之後發現還是被吊打,所以我們可能還忽略了什麼。
沒錯,這種方式,依舊存在缺點:
由於volatile關鍵字會遮蔽會虛擬機器中一些必要的程式碼優化,所以執行效率並不是很高。因此也建議,沒有特別的需要,不要大量使用。

筆者就遇到,使用這種模式,不知道什麼原因,第二次進入 activity的時候,view 刷不出來,然而資料物件什麼的都存在,調得我心力交瘁,欲生欲死,最後換了其他單例模式就ok了,希望懂的大俠告訴我一下,我只能懷疑volatile了。。。。。。

那你都這樣說了,那還怎麼玩,有沒有一種更好的方式呢?別急,往下看。

3、靜態式

什麼叫靜態式呢?回顧一下上面的餓漢式,我們再剛開始的就初始化了,不管你需不需要,而我們也說過,Java 再裝載類的時候,是不會併發的,那麼,我們能不能zuo做到懶載入,即需要的時候再去初始化,又能保證執行緒安全呢?當然可以,如下:

public class SingleMode{
    //構造方法私有化,這樣外界就不能訪問了
    private SingleMode(){
    };
    public static class Holder{
        private static SingleMode mSingleMode = new SingleMode();
        public static SingleMode getInstance(){
            return  mSingleMode;
        }
    }
}複製程式碼

除了上面的餓漢式和懶漢式,,靜態的好處在於能保證執行緒安全,不用去考慮太多、缺點就在於對引數的傳遞比較不好。
那麼這個時候,問題來了,引數怎麼傳遞?這個確實沒懶漢式方便,不過沒關係,我們可以定義一個init()就可以了,只不過初始化的時候多了一行程式碼;如:

public class SingleMode {
    //構造方法私有化,這樣外界就不能訪問了
    private SingleMode(){
    };
    public static class Holder{
        private static SingleMode mSingleMode = new SingleMode();
        public static SingleMode  getInstance(){
            return  mSingleMode;
        }
    }
    private Context mContext;
    public void init(Context context){
        this.mContext = context;
    }
}複製程式碼

初始化:

 SingleMode mSingleMode = SingleMode.Holder.getInstance();
 mSingleMode.init(this);複製程式碼

4、列舉單例

java 1.4 之前,我們習慣用靜態內部類的方式來實現單例模式,但在1.5之後,在 《Effective java》也提到了這個觀點,使用列舉的優點如下:

  • 執行緒安全
  • 延時載入
  • 序列化和反序列化安全

所以,現在一般用單個列舉的方式來實現單例,如上面,我們改一下:

public static SingleMode getInstance(){
        return Singleton.SINGLETON.getSingleTon();
    }
    public enum Singleton{
        SINGLETON ; //列舉本身序列化之後返回的例項,名字隨便取
        private AppUninstallModel singleton;

        Singleton(){ //JVM保證只例項一次
            singleton = new AppUninstallModel();
        }
        // 公佈對外方法
        public SingleMode getSingleTon(){
            return singleton;
        }
    }複製程式碼

好吧,這樣就ok了,但還是那個問題,初始化引數跟靜態類一樣,還是得重新寫個 init() 有失必有得吧。

這樣,我們的單例模式就學完了。

相關文章