簡介
這周繼續寫《Android源碼設計模式解析與實戰》讀書筆記。本書的第二章介紹了單例模式的各種實現方式,以及在 Android 源碼中的應用。
單例模式介紹
確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例。它的作用是避免產生多個對象消耗過多的資源,或者某種類型的對象只應該有且只有一個。比如創建一個對象需要消耗的資源過多,如要訪問 IO 和數據庫等資源。
單例模式使用要點
單例模式 UML 類圖如下:
實現單例模式主要有如下幾個關鍵點:
1.構造函數不對外開放,一般為 Private;
2.通過一個靜態方法或者枚舉返回單例對象;
3.確保單例類的對象只有一個,尤其是在多線程環境下(難點);
4.確保單例類對象在反序列化時不會重新構建對象。
單例模式用法
餓漢模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
餓漢模式在裝載類時就創建對象實例,是典型的空間換時間。
懶漢模式
public class Singleton {
private static Singleton instance;
private Singleton(){};
public static synchronized Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
懶漢模式在每次獲取實例時都會進行判斷,是典型的時間換空間。 getInstance() 方法中添加了 synchronized關鍵字,也就是上面所說的在多線程情況下保證單例對象唯一性的手段。但是即使 instance 已經被初始化,每次調用 getInstance() 方法都會進行同步,浪費不必要的資源,這也就是懶漢模式的最大問題。因此這種模式一般不建議使用。
雙重檢查鎖定(Double Check Lock)實現單例
public class DCLSingleton {
// JDK1.5后的版本才可使用volatile關鍵字,保證sInstance對象每次都從主內存中讀取
private volatile static DCLSingleton sInstance = null;
private DCLSingleton() {
}
public static DCLSingleton getInstance(){
if(sInstance==null){
synchronized(DCLSingleton.class){
if(sInstance==null){
sInstance=new DCLSingleton();
}
}
}
return sInstance;
}
}
這個寫法的特別之處在于對 instance 進行了兩次判空:第一層主要是為了避免不必要的同步,第二層則是為了在 null 的情況下創建實例。
我們會發現上面代碼有一個volatile關鍵字,因為在這里會有DCL失效問題。
DCL 失效問題:假設線程 A 執行到sInstance=new DCLSingleton()語句,這看上去像是一句代碼,實際上它并不是一個原子操作,這句代碼最終會被編譯為多條匯編指令,它大致做了三件事:
1.給 sInstance 的實例分配內存;
2.調用 DCLSingleton 的構造函數,初始化成員字段;
3.將 sInstance 對象指向分配的內存空間(此時 sInstance 就不是 null了)。
但是由于 Java 編譯器允許處理器亂序執行。因此執行順序可能是 1-2-3 也可能是 1-3-2,如果是后者,并且在 3 執行完畢、2 未執行之前被切換到 B 線程上,這時的 sInstance 因為已經在線程 A 內執行過了第三點,sInstance 已經是非空了,所以線程 B 直接取走 sInstance,再使用就會出錯,這就是 DCL 失效問題。
JDK 1.5 之后的版本具體化了 volatile 關鍵字,用它可以保證 sInstance 對象每次都從主內存中讀取,雖然會影響性能,這種方式第一次加載時會稍慢,在高并發環境會有缺陷,但是一般能夠滿足需求。
靜態內部類單例模式
public class Singleton implements Serializable{
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
/**
* 靜態內部類
*/
private static class SingletonHolder{
private static final Singleton sInstance=new Singleton();
}
/**
* 為了杜絕對象在反序列化時重新生成對象,則重寫Serializable的私有方法
* @return
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException{
return SingletonHolder.sInstance;
}
}
這種是推薦使用的單例模式實現方式。當第一次加載Singleton類時并不會初始化INSTANCE,只有在第一次調用getInstance方法時才會導致INSTANCE被初始化。這種方式不僅能夠保證線程安全,也能保證單例對象的唯一性,同時也延長了單例的實例化。
上面的代碼重寫了 readResolve() 方法,這是因為通過序列化可以將一個單例的實例對象寫到磁盤,然后讀回來,從而獲得一個實例。即使構造函數是私有的,反序列化時依然可以通過特殊的途徑去創建類的一個新的實例,相當于調用該類的構造函數。反序列化操作提供了一個特別的鉤子函數,類中具有一個私有的、被實例化的方法 readResolve(),這個方法可以讓開發人員控制對象的反序列化。重寫該方法返回 SingletonHolder.sInstance ,而不是默認的生成新的實例,從而保持單例。
枚舉單例
public enum SingletonEnum {
INSTANCE;
public void doSomething(){
System.out.println("do sth.");
}
}
這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。
容器實現單例
public class SingletonManager {
private static Map<String,Object> objMap=new HashMap<String,Object>();
private SingletonManager(){};
public static void registerService(String key,Object instance){
if(!objMap.containsKey(key)){
objMap.put(key, instance);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}
將多種單例類型注入到一個統一的管理類中,在使用時根據key獲取對象對應類型的對象。這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使用成本,也對用戶隱藏了具體實現,降低了耦合度。
單例模式運用場景
Windows 的 Task Manager (任務管理器)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能打開兩個 windows task manager 嗎? 不信你自己試試看哦~
windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統運行過程中,回收站一直維護著僅有的一個實例。
網站的計數器,一般也是采用單例模式實現,否則難以同步。
應用程序的日志應用,一般都何用單例模式實現,這一般是由于共享的日志文件一直處于打開狀態,因為只能有一個實例去操作,否則內容不好追加。
Web 應用的配置對象的讀取,一般也應用單例模式,這個是由于配置文件是共享的資源。
數據庫連接池的設計一般也是采用單例模式,因為數據庫連接是一種數據庫資源。數據庫軟件系統中使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因為何用單例模式來維護,就可以大大降低這種損耗。
多線程的線程池的設計一般也是采用單例模式,這是由于線程池要方便對池中的線程進行控制。
操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統。
HttpApplication 也是單位例的典型應用。熟悉 ASP.Net(IIS) 的整個請求生命周期的人應該知道 HttpApplication 也是單例模式,所有的 HttpModule 都共享一個 HttpApplication 實例.
總結以上,不難看出:
單例模式應用的場景一般發現在以下條件下:
(1)資源共享的情況下,避免由于資源操作時導致的性能或損耗等。如上述中的日志文件,應用配置。
(2)控制資源的情況下,方便資源之間的互相通信。如線程池等。
Android源碼中的單例模式
在 Android 系統中,我們經常會通過 Context 獲取系統級別的服務,如 WindowsManagerService、ActivityManagerService 等,更常用的是一個 LayoutInflater 的類,這些服務會在合適的時候以單例的形式注冊在系統中,在我們需要的時候就通過 Context 的 getSystemService(String name) 獲取。
總結
優點:
1.由于單例模式在內存中只有一個實例,減少了內存開支,特別是一個對象需要頻繁的創建、銷毀時,而且創建或銷毀時性能又無法優化,單例模式的優勢就非常明顯。
2.單例模式可以避免對資源的多重占用,例如一個文件操作,由于只有一個實例存在內存中,避免對同一資源文件的同時操作。
3.單例模式可以在系統設置全局的訪問點,優化和共享資源訪問,例如,可以設計一個單例類,負責所有數據表的映射處理。
缺點:
1.單例模式一般沒有接口,擴展很困難,若要擴展,只能修改代碼來實現。
2.單例對象如果持有 Context,那么很容易引發內存泄露。此時需要注意傳遞給單例對象的 Context 最好是 Application Context。
參考資料
《Android 源碼設計模式解析與實戰 》