Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開發中單例模式寫法與可能遇到的坑

Android開發中單例模式寫法與可能遇到的坑

編輯:關於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;     
   }    
}  
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved