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

作者 : 夏至 歡迎轉(zhuǎn)載,也請保留這段申明
http://blog.csdn.net/u011418943/article/details/60139644

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

單例模式的定義就不解釋過多了,相信很多小伙伴在設(shè)計的時候,都用到這個模式;常用的場景為 數(shù)據(jù)庫的訪問,文件流的訪問以及網(wǎng)絡(luò)連接池的訪問等等,在這些場景中,我們都希望實例只有一個,除了減少內(nèi)存開銷之外,也防止防止多進程修改文件錯亂和數(shù)據(jù)庫鎖住的問題。
在這一篇文章中,我將帶你分析 android 常見的集中單例模式,并詳細(xì)分析他們的優(yōu)缺點。讓大家在以后的選擇單例中,可以根據(jù)實際情況選擇。
當(dāng)然,如有錯誤,也歡迎指正。
下面是介紹:

1、餓漢式

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

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

餓漢式的好處是線程安全,因為虛擬機保證只會裝載一次,再裝載類的時候,是不會并發(fā)的,這樣就保證了線程安全的問題。
但缺點也很明顯,一初始化就實例占內(nèi)存了,但我褲子還沒脫,不想用呢。

2、懶漢式

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

public class SingleMode{
    //構(gòu)造方法私有化,這樣外界就不能訪問了
    private SingleMode(){
    };
    private static SingleMode mSingleMode;
    public static SingleModegetInstance(){ //這里就是延時加載的意思
        if (mSingleMode == null){
            mSingleMode = new SingleMode();
        }
        return  mSingleMode;
    }
}

懶漢式如上所示,
優(yōu)點:

我們只需要在用到的時候,才申請內(nèi)存,且可以從外部獲取參數(shù)再實例化,這點是懶漢式的最大優(yōu)點了

缺點:

單線程只實例了一次,如果是多線程了,那么它會被多次實例

至于問什么說它是線程不安全的呢?先下面這張圖:

我們假設(shè)一下,有兩個線程,A和B都要初始化這個實例;此時 A 比較快,已經(jīng)判斷 mSingleMode 為null,正在創(chuàng)建實例,而 B 這時候也再判斷,但此時 A 還沒有 new 完,所以 mSingleMode 還是為空的,所以B 也開始 new 出一個對象出來,這樣就相當(dāng)于創(chuàng)建了兩個實例了,所以,上面這種設(shè)計并不能保證線程安全。

2.1、如何實現(xiàn)懶漢式線程安全?

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

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

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

在上面的基礎(chǔ)上,用了二次檢查,這樣就保證了線程安全了,它會先判斷是否為null,是才會去加載,而且用 synchronized 修飾,則又保證了線程安全。

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

  • 1、為 mSingleMode 創(chuàng)建內(nèi)存
  • 2、new SingleMode() 調(diào)用這個構(gòu)造方法
  • 3、mSingleMode 指向內(nèi)存區(qū)域

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

  • 1、為 mSingleMode 創(chuàng)建內(nèi)存
  • 3、mSingleMode 指向內(nèi)存區(qū)域
  • 2、new SingleMode() 調(diào)用這個構(gòu)造方法

那這個時候,mSingleMode 已經(jīng)指向內(nèi)存區(qū)域了,那這個時候它就不為 null了,而實際上它并未獲得構(gòu)造方法,比如構(gòu)造方面里面有些參數(shù)或者方法,但是你并未獲取,然而這個時候線程B過來,而 mSingleMode已經(jīng)指向內(nèi)存區(qū)域不為空了,但方法和參數(shù)并未獲得, 所以,這樣你線程B在執(zhí)行 mSingleMode 的某些方法時就會報錯。

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

public class SingleMode{
    //構(gòu)造方法私有化,這樣外界就不能訪問了
    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;
    }
}

看到這里,是不是感覺爬了幾百盤的坑,終于上了黃金段位了。。。
然而,并不是,你打了排位之后發(fā)現(xiàn)還是被吊打,所以我們可能還忽略了什么。
沒錯,這種方式,依舊存在缺點:
由于volatile關(guān)鍵字會屏蔽會虛擬機中一些必要的代碼優(yōu)化,所以運行效率并不是很高。因此也建議,沒有特別的需要,不要大量使用。

筆者就遇到,使用這種模式,不知道什么原因,第二次進入 activity的時候,view 刷不出來,然而數(shù)據(jù)對象什么的都存在,調(diào)得我心力交瘁,欲生欲死,最后換了其他單例模式就ok了,希望懂的大俠告訴我一下,我只能懷疑volatile了。。。。。。

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

3、靜態(tài)式

什么叫靜態(tài)式呢?回顧一下上面的餓漢式,我們再剛開始的就初始化了,不管你需不需要,而我們也說過,Java 再裝載類的時候,是不會并發(fā)的,那么,我們能不能zuo做到懶加載,即需要的時候再去初始化,又能保證線程安全呢?當(dāng)然可以,如下:

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

除了上面的餓漢式和懶漢式,,靜態(tài)的好處在于能保證線程安全,不用去考慮太多、缺點就在于對參數(shù)的傳遞比較不好。
那么這個時候,問題來了,參數(shù)怎么傳遞?這個確實沒懶漢式方便,不過沒關(guān)系,我們可以定義一個init()就可以了,只不過初始化的時候多了一行代碼;如:

public class SingleMode {
    //構(gòu)造方法私有化,這樣外界就不能訪問了
    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 之前,我們習(xí)慣用靜態(tài)內(nèi)部類的方式來實現(xiàn)單例模式,但在1.5之后,在 《Effective java》也提到了這個觀點,使用枚舉的優(yōu)點如下:

  • 線程安全
  • 延時加載
  • 序列化和反序列化安全

所以,現(xiàn)在一般用單個枚舉的方式來實現(xiàn)單例,如上面,我們改一下:

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了,但還是那個問題,初始化參數(shù)跟靜態(tài)類一樣,還是得重新寫個 init() 有失必有得吧。

這樣,我們的單例模式就學(xué)完了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容