Android備忘錄《單例模式》

Ansong發表於2018-06-12

什麼是單例:單例模式能夠保證整個應用中有且只有一個例項,並且只能夠通過其暴露的方法獲取例項,不能夠自由構造。

解決的問題是:可以保證一個類在記憶體中的物件的唯一性,在一些常用的工具類、執行緒池、快取,資料庫,賬戶登入系統、配置檔案等程式中可能只允許我們建立一個物件,一方面如果建立多個物件可能引起程式的錯誤,另一方面建立多個物件也造成資源的浪費。

1、餓漢式(佔資源少的情況下推薦使用)

public class SingleTon {

    private static SingleTon instance = new SingleTon();
    
    private SingleTon(){
        
    }
    
    public static SingleTon getInstance(){
        return instance;
    }
    
}
複製程式碼

適合那些在初始化時就要用到單例的情況,如果單例物件初始化非常快,而且佔用記憶體非常小的時候這種方式是比較合適的。如果單例初始化的操作耗時比較長而應用對於啟動速度又有要求,或者單例的佔用記憶體比較大,再或者單例只是在某個特定場景的情況下才會被使用,而一般情況下是不會使用時,使用「餓漢式」的單例模式就是不合適的,這時候就需要用到「懶漢式」的方式去按需延遲載入單例

優點:類載入的時候就完成了例項化,避免了執行緒的同步問題,獲取單例速度快。

缺點:由於在類載入的時候就例項化了,所以沒有達到LazyLoading(懶載入)的效果,如果沒有用到這個例項它也會載入,會造成記憶體的浪費(但是這個浪費可以忽略)。

2、懶漢式(執行緒不安全,不推薦使用)

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon(){

    }

    public static SingleTon getInstance(){
        if(instance == null){
            instance = new SingleTon();
        }
        return instance;
    }

}
複製程式碼

「懶漢式」與「餓漢式」的最大區別就是將單例的初始化操作,延遲到需要的時候才進行,這樣做在某些場合中有很大用處。比如某個單例用的次數不是很多,但是這個單例提供的功能又非常複雜,而且載入和初始化要消耗大量的資源,這個時候使用「懶漢式」就是非常不錯的選擇。

優點:延遲載入,不浪費資源。 缺點:是第一次載入時需要及時進行例項化,這種寫法在多執行緒模式下存在安全問題 (有多個執行緒去呼叫getInstance()方法來獲取Singleton的例項,那麼就有可能發生這樣一種情況當第一個執行緒在執行if(instance==null)這個語句時,此時instance是為null的進入語句。在還沒有執行instance=new Singleton()時(此時instance是為null的)第二個執行緒也進入if(instance==null)這個語句,因為之前進入這個語句的執行緒中還沒有執行instance=new Singleton(),所以它會執行instance=newSingleton()來例項化Singleton物件,因為第二個執行緒也進入了if語句所以它也會例項化Singleton物件。這樣就導致了例項化了兩個Singleton物件。)

3、懶漢式(執行緒安全,效率低,不推薦使用)

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon() {

    }

    public static SingleTon getInstance() {
        synchronized (SingleTon.class) {
            if (instance == null) {
                instance = new SingleTon();
            }
        }
        return instance;
    }

}
複製程式碼

之前提到懶漢式的單例寫法會有執行緒安全問題,這種方式通過增加同步鎖方式,來解決執行緒安全問題,但是新的問題會存在,就是在頻繁呼叫獲取單例時,會頻發檢查同步,而此時會比較耗時,導致效率低下。

4、式懶漢式雙重校驗鎖(推薦使用)

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon() {

    }

    public static SingleTon getInstance() {
        if(instance == null){
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }

}
複製程式碼

DCL 在getInstance 方法中 對instance 進行兩次判空:為什麼要判斷兩次,第一個判空是為了避免不必要的同步,第二層判斷是為了在null 情況下建立例項。instance=new Singleton(); 語句看起來是有程式碼,單實際是一個原子操作,最終會被編譯成多條彙編指令,大致做了三件事:

1.給Singleton 分配記憶體

2.呼叫Singleton 的建構函式,初始化成員欄位

3.將instance 物件指向分配的記憶體空間(此時instance 就不是null 了)

但是jdk 1.5 以後java 編譯器允許亂序執行 。所以執行順序可能是1-3-2 或者 1-2-3.如果是前者先執行3 的話 切換到其他執行緒,instance 此時 已經是非空了,此執行緒就會直接取走instance ,直接使用,這樣就回出錯。DCL 失效。解決方法 SUN 官方已經給我們了。

將instance 定義成 private volatile static Singleton instance =null

DCL 的優點,資源利用率高,第一次執行getInstance 時才會被例項化,效率高。

缺點:第一次載入反應慢,也由於java 記憶體 模型的原因偶爾會失敗,在高併發環境下,有一定缺陷,雖然發生概率很小。(很常用)

private static volatile Singleton instance = null; 這裡使用了volatile關鍵字,因為多個執行緒併發時初始化成員變數和物件例項化順序可能會被打亂,這樣就出錯了,volatile可以禁止指令重排序。雙重校驗雖然在一定程度解決了資源的消耗和多餘的同步,執行緒安全問題,但在某些情況還是會出現雙重校驗失效問題,即DCL失效,使用volatile可保證每次都從主記憶體中讀取,可能會導致效能問題。

5、靜態內部類實現單例

public class Singleton { 
	private Singleton() { 
	
	} 
	private static class SingletonHolder { 
		private static final Singleton singleton = new Singleton(); 
	} 
	public static Singleton getInstance() { 
		return SingletonHolder.singleton; 
	}
}
複製程式碼

這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化例項時只有一個執行緒。不同的地方在餓漢式方式是隻要Singleton類被裝載就會例項化,沒有Lazy-Loading的作用,而靜態內部類方式在Singleton類被裝載時並不會立即例項化,而是在需要例項化時,呼叫getInstance方法,才會裝載SingletonHolder類,從而完成Singleton的例項化。類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM幫助我們保證了執行緒的安全性,在類進行初始化時,別的執行緒是無法進入的。

優點:避免了執行緒不安全,延遲載入,效率高。

6、列舉單例

public enum SingletonEnum {  
      
     instance;   
       
     private SingletonEnum() {}  
       
     public void method(){
     
     }  
}  
複製程式碼

可以看到列舉的書寫非常簡單,訪問也很簡單SingletonEnum.instance.method();
更加簡潔,執行緒安全,還能防止反序列化導致重新建立新的物件,而以上方法還需提供readResolve方法,防止反序列化一個序列化的例項時,會建立一個新的例項。列舉單例模式,我們可能使用的不是很多,但《Effective Java》一書推薦此方法,說“單元素的列舉型別已經成為實現Singleton的最佳方法”。不過Android使用enum之後的dex大小增加很多,執行時還會產生額外的記憶體佔用,因此官方強烈建議不要在Android程式裡面使用到enum,列舉單例缺點也很明顯。

7、容器單例

public class SingletonManager { 
    private SingletonManager() { } 

    private static Map<String, Object> instanceMap = new HashMap<>(); 

    public static void registerInstance(String key, Object instance) { 
	    if (!instanceMap.containsKey(key)) { 
                instanceMap.put(key, instance); 
            } 
    } 

    public static Object getInstance(String key) {
        return instanceMap.get(key); 
    }
}


    public class SingletonPattern { 
        SingletonPattern() { } 
        public void doSomething() { 
            Log.d("wxl", "doSomeing"); 
        } 
    }
複製程式碼

程式碼呼叫:

SingletonManager.registerInstance("SingletonPattern", new SingletonPattern());
SingletonPattern singletonPattern = (SingletonPattern) SingletonManager.getInstance("SingletonPattern");
singletonPattern.doSomething();
複製程式碼

根據key獲取物件對應型別的物件,隱藏了具體實現,降低了耦合度。 將眾多單例模式型別注入到一個統一的管理類中,在使用時根據key 對應型別的物件。這種方式使得我們可以管理多種型別的單例,並且在使用時可以通過統一的介面進行獲取操作,降低了使用者的使用成本,也對使用者隱藏了具體實現,降低了耦合度。

相關文章