簡(jiǎn)介
這周繼續(xù)寫《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》讀書筆記。本書的第二章介紹了單例模式的各種實(shí)現(xiàn)方式,以及在 Android 源碼中的應(yīng)用。
單例模式介紹
確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。它的作用是避免產(chǎn)生多個(gè)對(duì)象消耗過(guò)多的資源,或者某種類型的對(duì)象只應(yīng)該有且只有一個(gè)。比如創(chuàng)建一個(gè)對(duì)象需要消耗的資源過(guò)多,如要訪問(wèn) IO 和數(shù)據(jù)庫(kù)等資源。
單例模式使用要點(diǎn)
單例模式 UML 類圖如下:

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