設計模式之--單例模式(7種寫法)

1、單例模式介紹

單例模式是應用最廣的設計模式之一,也可能是很多初級工程師唯一會使用的設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個全局對象,這樣有利於我們協調系統整體行爲。如在一個應用中,應該只有一個ImageLoader實例,這個ImageLoader中又包含有線程池,緩存系統、網絡請求等,很消耗資源,因此沒有理由讓它構造多個實例。這種不能自由構造對象的情況,就是單例模式的使用場景。

2、單例模式的定義

確保某個類有且只有一個對象的場景,避免產生多個對象消耗過多的資源,或者某種類型的對象應該有且只有一個。例如,創建一個對象需要消耗的資源過多,如需要訪問IO和數據庫等資源,這時就要考慮使用單例模式

3、單例模式的實現方式

3.1懶漢模式(線程安全)

懶漢模式是聲明一個靜態對象,並且在用戶第一次訪問getInstance時進行初始化,而上述的餓漢模式是在聲明靜態變量的時候就已經初始化。懶漢單例模式的實現如下。

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

你可能已經發現了,getInstance()方法中添加了synchronized關鍵字,也就是getInstance是一個同步方法,這就是上面所說的在多線程情況下保證單例對象唯一性的手段。細想一下,大家會發現一個問題,即使instance已經被初始化(第一次調用就會初始化instance),每次調用都會進行同步,這樣會消耗不必要的資源,這也是懶漢單例模式存在的最大的問題。
小結一下:懶漢單例模式的優點是單例只有在使用的時候纔會被實例化,在一定程度上節約了資源;缺點是第一次加載時需要即使進行實例化,反應稍慢,最大的問題是每次調用getInstance都進行同步,造成不必要的同步開銷。這種模式不建議使用

3.2 懶漢模式(線程不安全)
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(null==instance){
            instance = new Singleton();
        }
        return instance;
    }
}

聲明瞭一個靜態對象,在用戶第一次調用時初始化。這雖然節約了資源,但第一次加載時需要實例化,反應稍慢了一些,而且多線程時不能工作。

3.3 餓漢模式
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){        
        return instance;
    }
}

這種方式在類加載時就完成了初始化,所以類加載較慢,但是獲取對象的速度快。這種方式基於類加載機制,避免了多線程的同步問題。在類加載的時候就完成了實例化,沒有達到懶加載的效果。如果從始至終都沒用使用過這個實例,則會造成內存的浪費。

3.4 Double Check Lock(DCL)單例實現

DCL方式實現單例模式的優點是既能夠在需要時才初始化實例,又能夠保證線程安全,且單例對象初始化後調用getInstance不進行同步鎖,代碼如下所示:

public  class Singleton{
	private static volatile Singleton sInstance = null;
	private Singleton(){
	}
	public static Singleton getInstance(){
		if(mInstance == null){
			synchronized(Singleton.class){
				if(mInstance ==null){
					sInstance= new Singleton();
				}
			}
		}
		return sInstance;
	}
	public void doSomething(){
	  System.out.println("do sth.");
	}
}

本程序的亮點自然都在getInstance方法上,可以看出getInstance方法中對instance進行了兩次判空;第一層判空主要是爲了避免不必要的同步,第二層的判斷則是爲了在null的情況下創建實例。這是什麼意思呢?是不是有點摸不着頭腦,下面就一起來分析一下。
假設線程A執行到sInstance = new Singleton()語句,這裏看起來是一句話,但實際上它並不是一個原子操作,這代碼最終會被編譯成多條彙編指令,大致做了3件事:
(1)給Singleton的實例分配內存;
(2)調用Singleton()的構造函數,初始化成員字段;
(3)將sInstance對象指向分配的內存空間(此時sInstance就不是null了)
但是,由於java編譯器允許處理器亂序執行,以及JDK1.5之前JMM(Java Memory Modle,即Java內存模型)中Cache、寄存器到主內存回寫順序的規定,上面的第二和第三的順序無法保證。也就是說,執行的順序可能是1-2-3也可能是1-3-2。如果是後者,並且在3執行完畢、2未執行之前,切換到了線程B上,這時候sInstance因爲在線程A內執行過了第三點,sInstance已經是非空了,所以線程B直接取走sInstance,再使用就會出錯,這就是DCL失效的問題。而且這種難以跟蹤的錯誤很可能會隱藏很久。
在JDK1.5之後,SUN官方已經注意到這種問題,調整了JVM,具體化了volatile關鍵字,因此,如果JDK是1.5或者之前的版本,只需要將sInstance的定義改成private volatile static Singleton sInstance=null就可以保證sInstance對象每次都是從主內存中讀取,就可以使用DCL的寫法來完成單例模式。當然,volatile或多或少也會影響性能,但是考慮到程序的正確性,犧牲這點性能還是值得的。
DCL的優點:資源利用率高,第一次執行getInstance時單例對象纔會被初始化,效率高。缺點:第一次加載時反應稍慢,也由於Java內存模型的原因偶爾會失敗。在高併發環境下也有一定的缺陷,雖然發生的概率很小。DCL模式是使用最多的單例實現方式,它能夠在需要時才實例化單例對象,並且能夠在絕大多數場景下保證單例對象的唯一性,除非你的代碼在併發場景比較複雜或者低於JDK6版本下使用,否則這種方法一般能夠滿足需求。

3.5 靜態內部類單例模式

DCL雖然在一定程度上解決了資源消耗、多餘的同步、線程安全等問題,但是它還是在某些情況下出現失效的問題。這個問題被稱爲雙重檢查鎖定(DCL)失效,在《Java併發編程實踐》一書的最後談到了這個問題,並指出這種“優化”是醜陋的,不贊成使用的。而建議使用如下的代碼替代:

public class Singleton{
	 private Singleton(){}
	 public static Singleton getInstance(){
		return SingletonHolder.sInstance;
	}
	
	/**
	* 靜態內部類
	*/
	private static class SingletonHolder{
		private static final Singleton sInstance=new Singleton();
	}
}

當第一次加載Singleton類時並不會初始化sInstance,只有第一次調用Singleton的getInstance方法時才初始化sInstance。因此第一次調用會使虛擬機加載SingletonHolder類,這種方法不僅能夠確保線程安全,也能夠保證單例對象的唯一性,同時也延遲了單例的實例化,所以推薦使用這種方式實現單例模式

3.6枚舉單例

前面講了一些單例模式的實現方式,但是這些實現方法不是稍顯麻煩就是會在某些情況下出現問題,有沒有更簡單的實現方式呢?我們看看下面的實現:

public enum  Singleton {
    INSTANCE;
    public void doSomething(){
        System.out.println("do sth.");
    }
}

寫法簡單是枚舉單例的最大優點,枚舉在Java中與普通的類是一樣的,不僅能夠有字段,還能夠有自己的方法,最重要的是默認枚舉實例的創建都是線程安全的,並且在任何情況下它都是一個單例。

爲什麼這麼說呢?在上述的幾種單例模式的實現中,在一個情況下它們會出現重新創建對象的情況,那就是反序列化。
通過反序列化可以將一個單例的實例對象寫到磁盤,然後再讀回來,從而有效地獲得一個實例。即使構造函數是私有的,反序列化時依然可以通過特殊的途徑去創建類的一個新的實例,相當於調用該類的構造函數。反序列化操作提供了一個很特別的鉤子函數,類中具有一個私有的、被實例化的方法readResolve(),這個方法可以讓開發人員控制對象的反序列化。例如,在上述幾個示例中如果要杜絕單例對象在被反序列化時重新生成對象,那麼必須加入如下方法:

private Object readResolve() throws ObjectStreamException{
       return mSingleton;
   }

也就是在readResolve方法中將sInstance對象返回,而不是默認的重新生成一個新的對象。而對於枚舉,並不存在這個問題,因爲即使反序列化也不會重新生成新的實例。

3.7 使用容器實現單例模式

看了以上6種單例模式的實現之後,還有另一種實現:

public class   SingletonManager {
    private static Map<String, Object> objectMap = new HashMap<>(); 
    private SingletonManager(){
    }
    public static void registerService(String key,Object instance){
        if(!objectMap.containsKey(key)){
            objectMap.put(key, instance);
        }
    }
    public static Object getService(String key){
        return objectMap.get(key);
    }
}

在程序的初始,將多種單例類型注入到一個統一管理類中,在使用的時候根據key獲取對象對應的對象。這種方式使得我們可以管理多種類型的單例,並且在使用時,可以通過統一的接口進行獲取操作,降低了用戶的使用成本,也對用戶隱藏了具體實現,降低了耦合度。

不管用那種形式實現單例模式,它們的核心都是將構造函數私有化,通過靜態方法獲取一個唯一的實例,在獲取的過程中必須保證線程安全,防止反序列化導致重新生成實例對象等問題,具體選擇哪一種實現方式取決於項目本身,如,是否是複雜的併發環境、單例對象的資源消耗等。

單例模式是運用頻率很高的模式,但是由於在客戶端通常沒有高併發的情況,因此選擇哪種實現方式並不會有太大的影響。即便如此,推薦使用3.4 , 3.5使用的形式;

優點:

1.由於單例模式在內存中只有一個實例,減少了內存開支,特別是一個對象需要頻繁得創建銷燬時,而且創建或銷燬時性能又無法優化,單例模式的優勢就非常明顯。
2.由於單例模式只生成一個實例,所以減少了系統的性能開銷,當一個對象的產生需要比較多的資源時,如讀取配置、產生其他依賴對象時,則可以通過在應用啓動的時候直接產生一個單例對象,然後用永久駐留內存的方式來解決。
3.單例模式可以避免對資源的多重佔用,例如一個寫文件的操作,由於只用一個實例存在內存中,避免對同一個資源文件的同時操作。
4.單例模式可以在系統設置全局的訪問點,優化和共享資源訪問,例如,可以設計一個單例類,負責所有數據表的映射處理。

缺點:

1.單例模式一般沒有接口,擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑可以實現。
2.單例對象如果持有Context,那麼很容易引發內存泄漏,此時需要注意傳遞給單例對象的Context最好是ApplicationContext。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章