Android 單例模式的使用

有時候我們需要使用一個實用類A,這個類A專門提供一些公共功能供別人調用,而本身並不會處理業務邏輯。由於類A會被許多類乃至線程調用,假設我們的程序非常龐大,在運行的過程中,會訪問這個類A100次,爲了調用類A的方法,需要先創建A的對象,A a = new A()。這種方法在對A的訪問量較少的情況下沒問題,但是像我們這種情況,就會創建100個類A的實例,這100個實例是要佔用內存的,從這種角度來說,就造成了大量不必要的開銷。而單例模式,在整個程序生命週期中,只有一個實例,這樣就不會造成不必要的內存消耗。

單例模式的設計

爲了讓整個生命週期內只有一個實例,我們可以這樣做:
  1. public class Singleton {  
  2.   
  3.     private static Singleton sSingleton;  
  4.   
  5.     private Singleton() {  
  6.     }  
  7.   
  8.     public static Singleton getInstance() {  
  9.         if (sSingleton == null) {  
  10.             sSingleton = new Singleton();  // line A  
  11.         }  
  12.   
  13.         return sSingleton;  
  14.     }  
  15.   
  16. }  
上述做法好像沒啥問題,由於mSingleton是靜態的,因此能夠保證程序運行過程中只存在一個實例。但是針對多線程情況,就可能有問題,比如有2個線程同時併發調用getInstance方法,並且同時執行到了line A,這個時候還是會各自new一個對象出來,也就是說,存在了兩個實例,這違背了單例模式的概念,下面我們改進一下:
  1. public class Singleton {  
  2.   
  3.     private static Singleton sSingleton;  
  4.   
  5.     private Singleton() {  
  6.     }  
  7.   
  8.     public static Singleton getInstance() {  
  9.         synchronized (Singleton.class) {  
  10.   
  11.             if (mSingleton == null) {  
  12.                 sSingleton = new Singleton();  
  13.             }  
  14.             return sSingleton;  
  15.         }  
  16.   
  17.     }  
  18.   
  19. }  
上述做法的確是沒啥問題了,getInstance方法中對Singleton.class加鎖,可以保證同一時刻只有一個線程能夠進入getInstance方法。現在考慮一種情況,還是我們的比較龐大的工程,在某個變態的時刻,我們需要訪問Singleton對象100次,注意是高併發下的同時訪問,會是什麼情形呢?大概是這樣的:100個線程進入getInstance方法以後開始搶mSingleton的所有權,這個時候,有一個線程獲得了鎖,然後順利地得到了Singleton實例,接着會是什麼情形呢?應該是這樣的:剩下99個線程開始搶mSingleton的所有權,一直這樣類推下去,可能有一個線程運氣比較差,搶了100次才搶到鎖,程序的表現可能是這樣的:這個運氣差的線程被阻塞在getInstance方法中,遲遲無法返回,如果需要返回數據給ui的話,那麼ui將遲遲不會得到更新。

我們需要看一下上述代碼,真的需要每次進入getInstance方法都要獲得鎖嗎?其實不是的,整個Singleton類中,對mSingleton進行訪問的地方分爲兩類:讀和寫,而且僅當mSingleton爲null的時候纔會寫,mSingleton一旦創建完畢,後面就只剩下讀操作了,再怎麼高併發也沒什麼關係了,反正mSingleton已經是現成的,直接讀就可以了,看如下采用double-check機制的改進代碼:
  1. public class Singleton {  
  2.   
  3.     private volatile static Singleton sSingleton;  
  4.   
  5.     private Singleton() {  
  6.     }  
  7.   
  8.     public static Singleton getInstance() {  
  9.         if (sSingleton == null) { // line A  
  10.             synchronized (Singleton.class) { // line C  
  11.                 if (sSingleton == null)  
  12.                 sSingleton = new Singleton();  // line B  
  13.             }  
  14.         }  
  15.   
  16.         return sSingleton;  
  17.     }  
  18.   
  19. }  
上述代碼近乎完美,可以滿足幾乎所有場合(採用反射和類加載器另當別論)。上述代碼的好處在於:第一次創建實例的時候會同步所有線程,以後有線程再想獲取Singleton的實例就不需要進行同步,直接返回實例即可。還有double-check的意義在於:假設現在有2個線程A和B同時進入了getInstance方法,線程A執行到line A行,線程B執行到line B行,由於B線程還沒有初始化完畢,sSingleton還是null,於是線程A通過了sSingleton==null的判斷,並且往下執行,碰巧,當線程A執行到line C的時候,線程B初始化完畢了,然後線程B返回,注意,如果沒有double-check,這個時候線程A就執行到了line B,就會再次初始化sSingleton,這個時候Singleton實際上被new了兩次,已經不算完全意義上的單例了,而有了double-check,就會再進行一次爲null的判斷,由於B線程已經初始化了sSingleton,所以A線程就不會再次初始化sSingleton。

double-check(DCL)在很大程度上可以滿足高併發的需要,儘管如此,它還是有一些小缺點的,問題的關鍵在於儘管得到了Singleton的正確引用,但是卻有可能訪問到其成員變量的不正確值,這聽起來有點抽象,不過沒關係,我們只是需要有個感性的認識就好,如果你真的好奇,那麼請搜索“java happen-before”。既然DCL單例模式以中彩票的概率存在一些小問題,那麼有沒有所謂的完美的解決方案呢?答案是有,在給出之前,我要說的是:單例有很多種寫法,我們不能簡單地否定其他寫法,儘管它們看起來不能很好地處理高併發情況,也許它們本來就是用於低併發情形下的。下面請看一種所謂完美應用於高併發情形下的單例寫法,靜態內部類單例模式:
  1. public class Singleton  
  2. {  
  3.     private Singleton(){  
  4.     }  
  5.   
  6.     private static class InstanceHolder{  
  7.         private static final Singleton instance = new Singleton();  
  8.     }  
  9.   
  10.     public static Singleton getInstance(){  
  11.         return InstanceHolder.instance;  
  12.     }  
  13.   
  14. }  
就目前來看,DCL和靜態內部類單例模式是高併發場合首選的單例實現方式,在一些對併發要求不高的場合,我們也可以採用其他簡單的寫法,要做到具體情況具體分析,選擇適合的單例模式也是很有必要的,而不是一味地去追求高併發。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章