編輯:關於Android編程
年底了,手上的活不是很多,就想著將平時記錄的筆記總結一下。准備總結一下平時常常使用的設計模式。本篇就是比較常用的單例(Singleton)模式。
不管是Android開發還是Java開發,相信單例模式都是用的比較多的,平時再用的時候有沒有想過,到底有多少種寫法,或者有麼有什麼坑沒有踩呢?帶著這些問題我們先來了解一下什麼情況下會用到單例模式。
一般在希望系統中特定類只存在一個實例時,就可以使用單例模式。也就是說使用單例模式,最關心的是對象創建的次數,以及對象創建的時機。它的UML圖也是非常的簡單:
結構很簡單,但是我們再使用時,還是想要有一些要求的:
1.在調用getInstance()方法時返回一個且唯一的Singleton對象。
2.能夠在多線程使用時也能保證獲取的Singleton對象唯一
3.getInstance()方法的性能要保證
4.能在需要的時候才初始化,否則不用初始化
現在就來按照上邊的要求來實現吧。
寫法一 餓漢式
/** * 餓漢式 * 基於ClassLoader的機制,在同一classLoader下,該方式可以解決多線程同步的問題, * 但是該種單例模式沒有辦法實現懶加載 */ public class SingletonHungry { /** * 在ClassLoader加載該類時,就會初始化mInstance */ private static SingletonHungry mInstance = new SingletonHungry(); private SingletonHungry() { } public static SingletonHungry getInstance() { return mInstance; } }
以上就是餓漢式的寫法,滿足了上邊說的第1,2條要求。該模式有幾點要注意:
1.默認構造方法需要私有化,不然外部可以隨時的構造方法,這樣就沒法保證單例了。
2.SingletonHungry 類型的靜態變量mInstance也是私有化的。這樣外部就不能直接獲取到mInstance,並且正是由於mInstance是靜態變量並且聲明時就初始化了,我們知道根據java虛擬機和ClassLoader的特性,一個類在一個ClassLoader中只會被加載一次。並且這裡的mInstance在加載時就已經初始化了,這可以確定對象的唯一性。也就是說保證了在多線程並發情況下獲取到的對象是唯一的。
當然該種方式肯定也是有缺點的,就是不能滿足上邊要求中的第三點,例如某類實例需求依賴在運行時的參數來生成,那麼由於餓漢式在類加載時就已經初始化了,所以無法滿足懶加載。那我們就來看看懶加載的寫法。
* 寫法二 懶加載(非線程安全)*
/** * 懶漢式 * 只有在getInstance()時才會初始化mInstance * Created by chuck on 17/1/18. */ public class SingletonLazy { private static SingletonLazy mInstance; private SingletonLazy() { } public static SingletonLazy getInstanceUnLocked() { if (mInstance == null) {//line1 mInstance = new SingletonLazy();//line2 } return mInstance; }
可以看出確實是在調用getInstanceUnLocked()方法時,才會初始化實例,實現了懶加載。但是在能否滿足在多線程下正常工作呢?我們在這裡先分析一下假設有兩個線程ThreadA和ThreadB:
ThreadA首先執行到line1,這時mInstance為null,ThreadA將接著執行new SingletonLazy();在這個過程中如果mInstance已經分配了內存地址,但是還沒有完成初始化工作(問題就出在這兒,稍後分析),如果ThreadB執行了line1,因為mInstance已經指向了某一內存,所以將跳過new SingletonLazy()直接得到mInstance,但是此時mInstance還沒有完成初始化,那麼問題就出現了。造成這個問題的原因就是new SingletonLazy()這個操作不是原子操作。至少可以分解成以下上個原子操作:
1.分配內存空間
2.初始化對象
3.將對象指向分配好的地址空間(執行完之後就不再是null了)
其中第2,3步在一些編譯器中為了優化單線程中的執行性能是可以重排的。重排之後就是這樣的:
1.分配內存空間
3.將對象指向分配好的地址空間(執行完之後就不再是null了)
2.初始化對象
重排之後就有可能出現上邊分析的情況:
那麼既然這個方式不能保證線程安全,那我們之間加上同步不就可以了嗎?這確實也是一種方法
* 寫法三 懶加載(線程安全)*
/** * 懶漢式 * 只有在getInstance()時才會初始化mInstance * Created by chuck on 17/1/18. */ public class SingletonLazy { private static SingletonLazy mInstance; private SingletonLazy() { } /** * 方法名多了Locked表示是線程安全的,沒有其他意義 */ public synchronized static SingletonLazy getInstanceLocked() { if (mInstance == null) { mInstance = new SingletonLazy(); } return mInstance; } }
這裡和線程不安全的懶加載方式就是多了一個synchronized關鍵字,保證了線程安全,但是這又帶來了另外一個問題,性能問題。如果,有多個線程會頻繁調用getInstanceLocked()方法的話,可能會造成很大的性能損失。當然如果沒有多線程頻繁調用的話,就不存在多少性能損失了。那麼為了解決這個問題,有人提出了我們非常熟悉的雙重檢查鎖定(簡稱DCL)。
* 寫法四 雙重檢查鎖定(DCL)*
/** * 雙重檢查鎖定DCL * Created by chuck on 17/1/18. */ public class SingletonLazy { private static SingletonLazy mInstance; private SingletonLazy() { } public static SingletonLazy getInstance() { if (mInstance == null) {//第一次檢查 synchronized (SingletonLazy.class) {//加鎖 if (mInstance == null) {//第二次次檢查 mInstance = new SingletonLazy();//new 一個對象 } } } return mInstance; } }
在相當長的時間裡,我以為這個完美的平衡了並發和性能的問題,但後來看多有文章介紹,這個方法也是有問題的,而這個問題和上邊介紹過的重排問題一樣。還是舉ThreadA和ThreadB的例子:
當Thread經過第一次檢查對象為null時,會接著去加鎖,然後去執行new SingletonLazy(),上邊已經分析過了,改步驟存在重排現象,如果發生重排,即mInstance分配了內存地址,但是很沒有完成初始化工作,而此時ThreadB,剛好執行第一次檢查(沒有加鎖),mInstance已經分配了地址空間,不再為null,那麼ThreadB會獲取到沒有完成初始化的mInstance,這就出現了問題。當然方法還是有的,那就是volatile關鍵字(用法自己查吧)。在JDK1.5之後使用volatile關鍵字,將禁止上文中的三步操作重排,既然不會重排,也就不會出現問題了。
/** * 雙重檢查鎖定DCL * Created by chuck on 17/1/18. */ public class SingletonLazy { private volatile static SingletonLazy mInstance; private SingletonLazy() { } public static SingletonLazy getInstance() { if (mInstance == null) {//第一次檢查 synchronized (SingletonLazy.class) {//加鎖 if (mInstance == null) {//第二次次檢查 mInstance = new SingletonLazy();//new 一個對象 } } } return mInstance; } }
問題是解決了,但是volatile要在JDK1.5以上版本(JDK1.5之前的可以參考http://www.ibm.com/developerworks/cn/java/j-dcl.html)才能起作用,其還會屏蔽jvm做的代碼優化,這些有可能導致程序性能降低,並且目前為止DCL已經有一些復雜了。有沒有更簡單的方法呢?答案是有的
* 寫法五 靜態內部類*
/** * 靜態內部類方式實際上是結合了餓漢式和懶漢式的優點的一種方式 * Created by chuck on 17/1/18. */ public class SingletonInner { private SingletonInner() { } /** * 在調用getInstance()方法時才會去初始化mInstance * 實現了懶加載 * * @return */ public static SingletonInner getInstance() { return SingletonInnerHolder.mInstance; } /** * 靜態內部類 * 因為一個ClassLoader下同一個類只會加載一次,保證了並發時不會得到不同的對象 */ public static class SingletonInnerHolder { public static SingletonInner mInstance = new SingletonInner(); } }
這是一個很聰明的方式,結合了結合了餓漢式和懶漢式的優點,並且也不影響性能。為什麼這麼說?因為我們在單例類SingletonInner類中,實現了一個static的內部類SingletonInnerHolder,該類中定義了一個static的SingletonInner類型的變量mInstance,並且會在classLoader第一次加載SingletonInnerHolder這個類時進行初始化。這樣做的好處是在classLoader在加載單例類SingletonInner時不會初始化mInstance。只有在第一次調用SingletonInner的getInstance()方法時,classLoader才會去加載SingletonInnerHolder,並初始化mInstance,並且由於ClassLoader的機制,一個ClassLoader同一個類,只加載一次,那麼不管多少線程,得到的也是同一個類,保證了並發下是該方式是可用的。其缺點也是有的,有些語言不支持這種語法。
接下來在介紹一種很簡單的方式:
寫法六 枚舉
/** * Created by chuck on 17/1/18. */ public enum SingletonEnum { SINGLETON_ENUM; private SingletonEnum() { } }
就是這麼的簡單,改方式不僅能避免多線程並發同步的問題,而且還天生支持序列化,可以防止在反序列化時創建新的對象。是一種比較推薦的方式,在java中需要在JDK1.5以上才支持enum。
總結:單例模式還有其他的實現方法,熟悉Android的同學都知道,Handler機制中用到的ThreadLocal其實就使用了一種單例,就是在處理並發時,保證每一個線程都有一個單例實現。在上述介紹的各種方式中,沒有哪一個是絕對最好的,需要結合各自的情況決定。例如一般不要求懶加載的話,可以使用寫法一餓漢式,如果要求懶加載,如果明確需要懶加載的,再根據是否需要線程安全考慮選擇寫法二,三。如果單例類需要反序列化,那麼可以使用寫法六枚舉。總之,需要結合自己的實際情況來看。最後,再來看看幾個問題:
第一 、多ClassLoder情況,如果是多個ClassLoder都加載了單例類,那麼就會出現多個同名的對象,這違背了單例模式的原則。解決這個問題,就要保證只有一個ClassLoder加載單例類。
第二、單例類序列化問題,只要保證反序列化時,得到同一個對象就可以了,通過重寫readResolve()方法可以實現。
public class Singleton implements java.io.Serializable { ... private Object readResolve() { return mInstance; } }
硬盤緩存策略:LimitedAgeDiscCache(設定文件存活的最長時間,當超過這個值,就刪除該文件)UnlimitedDiscCac
比如我們有 2 個分支:master, dev,現在想查看這兩個 branch 的區別,有以下幾種方式:1.查看 dev 有,而 master 中沒有的:git log
在 Android 的 OnScrollListener 整個事件我主要分析下他的執行順序: 實現滾動事件的監聽接口 new AbsListView.OnScrol
活動的啟動模式啟動模式一共有四種,分別是 standard、singleTop、singleTask 和singleInstance,可以在AndroidManifest