前言
本文主要參考 那些年,我們一起寫(xiě)過(guò)的“單例模式”。
何為單例模式?
顧名思義,單例模式就是保證一個(gè)類(lèi)僅有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。通常我們可以讓一個(gè)全局變量使得一個(gè)對(duì)象被訪問(wèn),但它不能防止你實(shí)例化多個(gè)對(duì)象。一個(gè)最好的辦法就是,讓類(lèi)自身負(fù)責(zé)保存它的唯一實(shí)例。這個(gè)類(lèi)可以保證沒(méi)有其他實(shí)例可以被創(chuàng)建,并且它可以提供一個(gè)訪問(wèn)實(shí)例的方法。
結(jié)構(gòu)
適用情況
- 當(dāng)類(lèi)只能有一個(gè)實(shí)例而且客戶(hù)可以從一個(gè)眾所周知的訪問(wèn)點(diǎn)訪問(wèn)它時(shí)。
- 產(chǎn)生某對(duì)象會(huì)消耗過(guò)多的資源,為避免頻繁地創(chuàng)建與銷(xiāo)毀對(duì)象對(duì)資源的浪費(fèi)。如對(duì)數(shù)據(jù)庫(kù)的操作、訪問(wèn)IO、線程池、網(wǎng)絡(luò)請(qǐng)求等。
單例模式的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):可以減少系統(tǒng)內(nèi)存開(kāi)支,減少系統(tǒng)性能開(kāi)銷(xiāo),避免對(duì)資源的多重占用、同時(shí)操作。
- 缺點(diǎn):擴(kuò)展困難,容易引發(fā)內(nèi)存泄露,測(cè)試?yán)щy,一定程度上違背了單一職責(zé)原則,進(jìn)程被殺死時(shí)可能狀態(tài)不一致問(wèn)題。
單例的各種實(shí)現(xiàn)
單例模式按加載時(shí)機(jī)可以分為:餓漢模式和懶漢模式;按實(shí)現(xiàn)的方式有雙重檢查加鎖方式,內(nèi)部類(lèi)方式、枚舉方式以及通過(guò)Map容器來(lái)管理單例的模式。作為一個(gè)單例,首先要確保的就是實(shí)例的“唯一性”,但是有很多因素如多線程、序列化、反射、克隆等因素會(huì)導(dǎo)致“唯一性”失效。其中,多線程問(wèn)題尤為突出。所以,我們應(yīng)該保證無(wú)論是在單線程還是多線程下單例模式都是可以運(yùn)行的。
-
懶漢式線程不安全方式
public class Singleton{ private static Singleton instance; //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化 private Singleton() { } //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例 public static Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; } }
這種寫(xiě)法只能在單線程中使用。如果是多線程,可能發(fā)生一個(gè)線程通過(guò)并進(jìn)入了if(instance == null)判斷語(yǔ)句快,但還未來(lái)得及創(chuàng)建新的實(shí)例時(shí),另一個(gè)線程也通過(guò)了這個(gè)判斷語(yǔ)句,兩個(gè)線程最終都進(jìn)行了創(chuàng)建,導(dǎo)致了多個(gè)實(shí)例的產(chǎn)生。所以這種方式在多線程下不適用。
-
線程安全效率低方式
public class Singleton { private static Singleton instance; //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化 private Singleton() { } //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例 public static synchronized Singleton getInstance() { //通過(guò)加上synchronized修飾符解決多線程不安全問(wèn)題 if (instance == null) { instance = new Singleton(); } return instance; } }
這種方式雖然解決了線程安全問(wèn)題,但是這樣迫使每個(gè)線程在進(jìn)入這個(gè)方法之前,要先等待其他的線程離開(kāi)該方法,即不會(huì)有兩個(gè)線程同時(shí)進(jìn)入此方法進(jìn)行 new Singleton(),從而保證了單例的有效性。但是當(dāng)每個(gè)線程每次執(zhí)行g(shù)etInstance()方法獲取類(lèi)的實(shí)例時(shí),都會(huì)進(jìn)行同步。而事實(shí)上當(dāng)實(shí)例創(chuàng)建完成后,同步就變?yōu)椴槐匾拈_(kāi)銷(xiāo)了,這樣做在高并發(fā)下必然會(huì)拖垮性能。
-
同步代碼塊方式
public class Singleton { private static Singleton instance; //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化 private Singleton() { } //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例 public static Singleton getInstance() { if (instance == null) { //僅同步實(shí)例化的代碼塊 synchronized (Singleton.class){ instance = new Singleton(); } } return instance; } }
但是這種同步并不能做到線程安全,同最初的懶漢模式一個(gè)道理,它可能產(chǎn)生多個(gè)實(shí)例,所以亦不可行。我們必須再增加一個(gè)單例不為空的判斷來(lái)保證線程安全,也就是所謂的“雙重檢查鎖定(Double Check Lock(DCL))”
-
雙重檢查鎖定(Double Check Lock(DCL))方式
public class Singleton { //注意此處的volatile修飾符 //Java編譯器允許處理器亂序執(zhí)行,會(huì)有DCL失效的問(wèn)題 //JDK大于等于1.5的版本,具體化了volatile關(guān)鍵字,定義時(shí)加上它可以保證執(zhí)行的順序(雖然會(huì)影響性能) //從而單例起效 private static volatile Singleton instance; //private的構(gòu)造函數(shù),只能在本類(lèi)內(nèi)部實(shí)例化 private Singleton() { } //通過(guò)此靜態(tài)方法提供全局獲取唯一可用對(duì)象的實(shí)例 public static Singleton getInstance() { if (instance == null) { //第一次check,避免不必要的同步 synchronized (Singleton.class) { //同步 if (instance == null) { //第二次check,保證線程安全 instance = new Singleton(); } } } return instance; } }
此方法的“Double-Check”體現(xiàn)了兩次 if(instance == null)的檢查,這樣既同步代碼塊保證線程安全,同時(shí)實(shí)例化的代碼只會(huì)執(zhí)行一次,實(shí)例化后同步不會(huì)再被執(zhí)行,從而提高效率。
雙重檢查鎖定(DCL)方式也延遲加載的,它唯一的問(wèn)題是Java編譯器允許處理器亂序執(zhí)行,在JDK版本低于1.5會(huì)有DCL失效的問(wèn)題。在版本大于等于1.5的JDK上,只需在定義單例時(shí)加上volatile關(guān)鍵字,即可保證執(zhí)行的順序,從而使單例起效。
-
延遲加載的靜態(tài)內(nèi)部類(lèi)
public class Singleton { private Singleton(){} public static final Singleton getInstance(){ return SingletonHolder.INSTANCE; } private static class SingletonHolder{ private static final Singleton INSTANCE = new Singleton(); } }
靜態(tài)內(nèi)部類(lèi)利用了classloader的機(jī)制來(lái)保證初始化 instance 時(shí)只會(huì)有一個(gè),這是因?yàn)樘摂M機(jī)會(huì)保證一個(gè)類(lèi)的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖、同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類(lèi),那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類(lèi)的<clinit>方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行 <clinit>() 方法完畢。如果在一個(gè)類(lèi)的 <clinit>() 方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)線程阻塞,這在實(shí)際應(yīng)用中往往是很隱蔽的。需要注意的是,其他線程雖然會(huì)被阻塞,但如果執(zhí)行 <clinit>() 方法的那條線程退出 <clinit>() 方法后,其他線程喚醒之后不會(huì)再次進(jìn)入 <clinit>() 方法。同一個(gè)類(lèi)加載器下,一個(gè)類(lèi)型只會(huì)初始化一次。
需要注意的是,雖然它的名字中有“靜態(tài)”兩字,但它屬于“懶漢模式”的。這種方式的Singleton類(lèi)被加載時(shí),因?yàn)閮?nèi)部靜態(tài)類(lèi)是要在有引用了之后才會(huì)裝載進(jìn)內(nèi)存,所以在第一次調(diào)用 getInstance()之前,換言之,只要 SingletonHolder 類(lèi)還沒(méi)有被主動(dòng)使用,instance 就不會(huì)被初始化。只有在顯示調(diào)用getInstance()方法時(shí),產(chǎn)生了對(duì)SingleHolder的引用才會(huì)加載SingltonHolder類(lèi),從而實(shí)例化對(duì)象。
-
餓漢加載方式
//優(yōu)點(diǎn)是比較簡(jiǎn)潔 public class Singleton{ //注意這里用的是public而不是private,因此無(wú)需getInstance()方法,可以直接 //拿到instance實(shí)例 //此方法的final關(guān)鍵詞來(lái)確保每次返回的都是同一個(gè)對(duì)象的引用,私有的構(gòu)造方法函數(shù) //也只會(huì)被調(diào)用一次 private Singleton(){} public static final Singleton instance = new Singleton(); } //Singleton with static factory //現(xiàn)代的JVM基本都內(nèi)嵌了對(duì)static factory //方法的調(diào)用,使得第一種public field方式不再有優(yōu)勢(shì) //此方法更靈活,只需修改getInstance的返回邏輯,而不需要 //改變API就可以將類(lèi)改為非單例類(lèi) public class Singleton { private Singleton(){} private static final Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } } public class Singleton { private Singleton() { } private static Singleton instance = null; static { instance = new Singleton(); } public static Singleton getInstance(){ return instance; } } public class Singleton { private Singleton() { } private static Singleton instance = null; static { instance = new Singleton(); } public static Singleton getInstance(){ return instance; } }
這三種方式差別不大,都依賴(lài)JVM在類(lèi)加載時(shí)就完成唯一對(duì)象的實(shí)例化,基于類(lèi)加載的機(jī)制,它們天生就是線程安全的,所以都是可行的,第二種更易于理解比較常見(jiàn)。
-
枚舉方式 關(guān)于枚舉
public enum Singleton { INSTANCE; //枚舉同Java中的普通Class一樣,能夠有自己的成員變量和方法 public void doSomething(){ System.out.println("Do whatever you want"); } }
枚舉類(lèi)型時(shí)有“實(shí)例控制”的類(lèi),確保了不會(huì)同時(shí)有兩個(gè)實(shí)例,即當(dāng)且僅當(dāng) a=b 時(shí) a.eaquals(b) ,用戶(hù)也可以用 == 操作符來(lái)代替 equals(Object) 方法來(lái)提供效率。使用枚舉來(lái)實(shí)現(xiàn)單例還可以不用getInstance()方法(當(dāng)然,如果你想要適應(yīng)大家的習(xí)慣用法,加上 getInstance()方法也是可以的),直接通過(guò)Singeton.INSTANCE 來(lái)拿取實(shí)例。枚舉類(lèi)是在第一次訪問(wèn)時(shí)才被實(shí)例化,是懶加載的。它寫(xiě)法簡(jiǎn)單,并確保了在任何情況下(包括反序列化,反射,克?。┫露际且粋€(gè)單例。不過(guò)枚舉是在JDK1.5之后才加入的特性。
其他需要注意的對(duì)單例模式的破壞
-
除了多線程,序列化也可能破壞單例模式一個(gè)實(shí)例的要求。二是實(shí)現(xiàn)對(duì)象數(shù)據(jù)的遠(yuǎn)程傳輸。當(dāng)單例對(duì)象有必要實(shí)現(xiàn)Serializable接口時(shí),即使將其構(gòu)造函數(shù)設(shè)為私有,在它反序列化時(shí)依然會(huì)通過(guò)特殊的途徑再創(chuàng)建類(lèi)的一個(gè)新的實(shí)例,相當(dāng)于調(diào)用了該類(lèi)的的構(gòu)造函數(shù)有效地獲得除了一個(gè)新的實(shí)例。
public class Singleton implements Serializable{ private static Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; } public static void main(String[] args) { Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); System.out.println("normal:" + (instance1 == instance2)); try { //序列化 File file = new File("tt.txt"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(instance1); oos.close(); //反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Singleton instance3 = (Singleton) ois.readObject(); System.out.println("deserialize:" + (instance1 == instance3)); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
輸出如下:
normal:true
deserialize:false
要比避免單例對(duì)象在反序列化重新生成對(duì)象,則在implements Serializable的同時(shí)應(yīng)該實(shí)現(xiàn)readResolve()方法,并在其中保證反序列化的時(shí)候獲得原來(lái)的對(duì)象。
(注:readResolve()是反序列化操作提供的一個(gè)很特別的鉤子函數(shù),它在從流中讀取對(duì)象的readObject(ObjectInputStream)方法之后被調(diào)用,可以讓開(kāi)發(fā)人員控制對(duì)象的反序列化。
public class Singleton implements Serializable{
......
private Object readResolve(){
return instance; //默認(rèn)返回 instance 對(duì)象,而不是重新生成一個(gè)新的對(duì)象
}
......
}
單例有效。
2.反射
除了多線程、反序列化以外,反射也會(huì)對(duì)單例造成破壞。反射可以通過(guò)setAccessible(true)來(lái)繞過(guò) private 機(jī)制,從而調(diào)用到類(lèi)的私有構(gòu)造函數(shù)創(chuàng)建對(duì)象。如下代碼所示:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
public static void main(String[] args) {
Singleton getInstance1 = Singleton.getInstance();
Singleton getInstance2 = Singleton.getInstance();
System.out.println("Is singleton pattern normally valid: " + (getInstance1 == getInstance2));
try {
/* Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.designpatterns.Singleton");
Constructor<Singleton> constructor = clazz.getConstructor(null); //獲得無(wú)參構(gòu)造函數(shù)*/
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); //跳過(guò)權(quán)限檢查,可以訪問(wèn)私有的構(gòu)造函數(shù)
Singleton refInstance3 = constructor.newInstance();
System.out.println("Is single pattern valid for Reflection: " + (getInstance1 == refInstance3));
} catch (Exception e) {
e.printStackTrace();
}
}
}
將會(huì)打?。?/p>
Is singleton pattern normally valid: true
Is single pattern valid for Reflection: false
說(shuō)明使用反射利用私有構(gòu)造器也是可以破壞單例的,要防止此情況發(fā)生,可以在私有的構(gòu)造器中加一個(gè)判斷,需要?jiǎng)?chuàng)建的對(duì)象不存在就創(chuàng)建;存在則說(shuō)明是第二次調(diào)用,拋出 RuntimeException 提示。代碼如下
public class Singleton{
......
private Singleton(){
//也可以在這里使用 flag 或者 計(jì)數(shù)器 count 來(lái)判斷
if(null != instance){
throw new RuntimeException("Cann't construct a Singleton more than once!");
}
}
}
同反序列化相似,枚舉的方式也可以杜絕反射的破壞。當(dāng)我們通過(guò)反射方式來(lái)創(chuàng)建枚舉類(lèi)型的實(shí)例時(shí),會(huì)拋出異常。
- 克隆
clone()是 Object 的方法,每一個(gè)對(duì)象都是 Object 的子類(lèi),都有 clone() 方法。 clone() 方法并不是調(diào)用構(gòu)造函數(shù)來(lái)創(chuàng)建對(duì)象,而是直接拷貝內(nèi)存區(qū)域。因此當(dāng)我們的單例對(duì)象實(shí)現(xiàn)了 Cloneable 接口時(shí),盡管其構(gòu)造函數(shù)時(shí)私有的,仍可以通過(guò)克隆來(lái)創(chuàng)建一個(gè)新對(duì)象,單例模式也相應(yīng)的失效了。
public class Singleton implements Cloneable{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
Singleton getInstance1 = Singleton.getInstance();
Singleton getInstance2 = Singleton.getInstance();
System.out.println("Is singleton pattern normally valid: " + (getInstance1 == getInstance2));
try {
Singleton cloneInstance3 = (Singleton) getInstance1.clone();
System.out.println("Is singleton pattern valid for clone: " + (getInstance1 == cloneInstance3));
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
輸出為:
Is singleton pattern normally valid: true
Is singleton pattern valid for clone: false
所以單例模式是不可以實(shí)現(xiàn) Cloneable 接口的,這與 Singleton 模式的初衷相違背。那要如何阻止使用 clone() 方法創(chuàng)建單例實(shí)例的另一個(gè)實(shí)例?可以 override 它的 clone() 方法,使其拋出異常。(也許你想問(wèn)既然知道了某個(gè)類(lèi)是單例且單例不應(yīng)該實(shí)現(xiàn) Cloneable 接口,那不實(shí)現(xiàn)該接口不就可以了嗎?事實(shí)上盡管很少見(jiàn),但有時(shí)候單例類(lèi)可以繼承其他類(lèi),如果父類(lèi)實(shí)現(xiàn)了 clone() 方法的話(huà),就必須在我們的單例中重寫(xiě)clone() 方法來(lái)阻止對(duì)單例的破壞。)
public class Singleton implements Cloneable{
......
@Override
protected Object clone() throws CloneNotSupportException{
throw new CloneNotSupportException();
}
......
}
P.S. Enum 是沒(méi)有 clone() 方法的
登記式單例——使用Map容器管理單例模式
采用Map容器來(lái)統(tǒng)一管理這些單例,使用時(shí)通過(guò)統(tǒng)一的接口來(lái)獲取某個(gè)單例。將一組單例類(lèi)型注入到一個(gè)統(tǒng)一的管理類(lèi)中來(lái)維護(hù),即將這些實(shí)例存放在一個(gè)Map登記簿中,在使用時(shí)則根據(jù) key 來(lái)獲取對(duì)象對(duì)應(yīng)類(lèi)型的單例對(duì)象。對(duì)于已經(jīng)登記過(guò)的實(shí)例,從 Map 直接返回實(shí)例;對(duì)于沒(méi)有登記的,則先登記再返回。從而在對(duì)用戶(hù)隱藏具體實(shí)現(xiàn)、降低代碼耦合度的同時(shí),也降低了用戶(hù)的使用成本。簡(jiǎn)易版代碼實(shí)現(xiàn)如下
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);
}
}
Android 的系統(tǒng)核心服務(wù)就是如上形式存在的,以達(dá)到減少資源消耗的目的。其中最為大家所熟知的服務(wù)有 LayoutInflater Service,它就是在虛擬機(jī)第一次加載ContextImpl 類(lèi)時(shí),以單例形式注冊(cè)到系統(tǒng)中的一個(gè)服務(wù),其他系統(tǒng)級(jí)的服務(wù)還有:WindowManagerService、ActivityManagerService 等。JVM 第一次加載調(diào)用 ContextImpl 的 registerService()方法,將這些服務(wù)以鍵值對(duì)的形式(以service name 為鍵,值則是對(duì)應(yīng)的ServiceFetcher)存儲(chǔ)在一個(gè)HashMap中,要使用時(shí)通過(guò)key拿到所需的 ServiceFetcher 后,再通過(guò) ServiceFetcher 的 getService()方法來(lái)獲取具體的服務(wù)對(duì)象。在第一次使用服務(wù)時(shí),SercviceFetcher 調(diào)用 createService()方法創(chuàng)建服務(wù)對(duì)象,并緩存到一個(gè)列表中,下次再取時(shí)就可以直接從緩存中獲取,無(wú)需重復(fù)創(chuàng)建對(duì)象,從而實(shí)現(xiàn)單例的效果。
關(guān)于單例模式的其他問(wèn)題(Q & A)
還有其他情況會(huì)使單例模式失效嗎?
上述的所有討論都是基于一個(gè)類(lèi)加載器(class loader)的情況。由于每個(gè)類(lèi)加載器有各自的命名空間, static 關(guān)鍵詞的作用范圍也不是整個(gè) JVM,而之到類(lèi)加載器,即不同的類(lèi)加載器可以加載同一個(gè)類(lèi)。所以當(dāng)一個(gè)工程下面存在不止一個(gè)類(lèi)加載器時(shí),整個(gè)程序中同一個(gè)類(lèi)就可能被加載多次,如果這是個(gè)單例類(lèi)就會(huì)產(chǎn)生多個(gè)單例并存失效的現(xiàn)象,并要指定同一個(gè)類(lèi)加載器。單例的構(gòu)造函數(shù)時(shí)私有的,那還能不能繼承單例?
單例是不適合被繼承的,要繼承單例就要將構(gòu)造函數(shù)改成公開(kāi)的或受保護(hù)的(僅考慮Java中的情況),這就會(huì)導(dǎo)致:
1)別的類(lèi)也可以實(shí)例化它了,無(wú)法確保保實(shí)例“獨(dú)一無(wú)二”,這顯然有違單例的設(shè)計(jì)理念。
2)因?yàn)閱卫膶?shí)例是使用的靜態(tài)變量,所有的派生類(lèi)事實(shí)上是共享同一個(gè)實(shí)例變量的,這種情況下要想讓子類(lèi)們維護(hù)正確的狀態(tài),順利工作,基類(lèi)就不得不實(shí)現(xiàn)注冊(cè)表功能了。單例有沒(méi)有違反“單一責(zé)任原則”?
單例確實(shí)承擔(dān)了兩個(gè)責(zé)任,它不僅僅負(fù)責(zé)管理自己的實(shí)例并提供全局訪問(wèn),還要處理應(yīng)用程序的某個(gè)業(yè)務(wù)邏輯。但是有類(lèi)來(lái)管理自己的實(shí)例的方式可以讓整體設(shè)計(jì)更簡(jiǎn)單易懂。
當(dāng)然在代碼繁復(fù)的情況下優(yōu)化你的設(shè)計(jì),讓單例類(lèi)專(zhuān)注于自己的業(yè)務(wù)責(zé)任,將它的實(shí)例化以及對(duì)對(duì)象個(gè)數(shù)的控制封裝在一個(gè)工廠類(lèi)或生成器中,也是較好的解決方法。是否可以把一個(gè)類(lèi)的所有方法和變量都定義為靜態(tài)的,把此類(lèi)直接當(dāng)作單例來(lái)使用?
事實(shí)上在最開(kāi)始討論過(guò)的,Java里的 java.lang.System 以及 java.lang.Math 類(lèi)都是這么做的,它們的全部方法都用 static 關(guān)鍵詞修飾,包裝起來(lái)提供類(lèi)級(jí)訪問(wèn)??梢钥吹?,Math 類(lèi)的把 java 基本類(lèi)型值運(yùn)算的相關(guān)方法組織了起來(lái),當(dāng)我們調(diào)用 Math 類(lèi)的某個(gè)類(lèi)方法時(shí),所要做的都只是數(shù)據(jù)操作,并不涉及到對(duì)象的狀態(tài),對(duì)這樣的工具類(lèi)來(lái)說(shuō)實(shí)例化是沒(méi)有任何意義的。
靜態(tài)方法會(huì)比一般的單例更快,因?yàn)殪o態(tài)的綁定是在編譯期就進(jìn)行的。但是也要注意到,靜態(tài)初始化的控制權(quán)完全握在 Java 手上,當(dāng)涉及到很多類(lèi)時(shí),這么做可能會(huì)一起一些微妙而不易察覺(jué)的,和初始化次序有關(guān)的bug。除非絕對(duì)必要,確保一個(gè)對(duì)象只有一個(gè)實(shí)例,會(huì)比類(lèi)只有一個(gè)單例更保險(xiǎn)。考慮技術(shù)實(shí)現(xiàn)時(shí),如何從單例模式和全局變量中作出選擇?
全局變量雖然使用起來(lái)比較簡(jiǎn)單,但對(duì)于單例有如下缺點(diǎn):
1) 全局變量只是提供了對(duì)象的全局靜態(tài)引用,但并不能確保只有一個(gè)實(shí)例。
2) 全局變量時(shí)急切實(shí)例化的,在程序一開(kāi)始就創(chuàng)建好對(duì)象,對(duì)非常耗費(fèi)資源的對(duì)象,或是程序執(zhí)行過(guò)程中一直沒(méi)有用到的對(duì)象,都會(huì)形成浪費(fèi)。
3) 靜態(tài)初始化時(shí)可能信息不完全,無(wú)法實(shí)例化一個(gè)對(duì)象。即可能需要使用到程序中稍后計(jì)算出來(lái)的值才能創(chuàng)建單例。
4) 使用全局變量容易造成命名空間污染。可以用單例對(duì)象 Application 來(lái)解決組件傳遞數(shù)據(jù)的問(wèn)題嗎?
在 Android 應(yīng)用啟動(dòng)后、任意組件被創(chuàng)建前,系統(tǒng)會(huì)自動(dòng)為應(yīng)用創(chuàng)建一個(gè) Application 類(lèi)(或其子類(lèi))的對(duì)象,且只創(chuàng)建一個(gè)。從此它一直在那里,直到應(yīng)用的進(jìn)程被殺掉。所以雖然 Application 并沒(méi)有采用單例模式來(lái)實(shí)現(xiàn),但是由于它的生命周期由框架來(lái)控制,和整個(gè)應(yīng)用的保持一致,且確保了只有一個(gè),所以可以被看作是一個(gè)單例。
一個(gè) Android 應(yīng)用總有一些信息,譬如說(shuō)一次耗時(shí)計(jì)算的結(jié)果,需要被用在多個(gè)地方。如果將需要傳遞的對(duì)象塞到 intent 里或者存儲(chǔ)到數(shù)據(jù)庫(kù)里來(lái)進(jìn)行傳遞,存儲(chǔ)都要分別寫(xiě)代碼實(shí)現(xiàn),還是有點(diǎn)麻煩的。既然 Application (或繼承它的子類(lèi))對(duì)于 APP 中的所有 activity 和 service 都可見(jiàn),而且隨著 App 啟動(dòng),它自始至終都在那里,就不禁讓我們想到,何不利用 Application 來(lái)持有內(nèi)部變量,從而實(shí)現(xiàn)在各組件間傳遞、分享數(shù)據(jù)呢?這看上去方便又優(yōu)雅,但卻是完全錯(cuò)誤的一種做法!如果你使用了如上做法,那你的應(yīng)用最終要么因?yàn)槿〔坏綌?shù)據(jù)發(fā)生 NullPointerException 而崩潰,要么就是取到了錯(cuò)誤的數(shù)據(jù)。這是因?yàn)?Application 不會(huì)永遠(yuǎn)駐留在內(nèi)存里,隨著進(jìn)程被殺掉,Application 也會(huì)被銷(xiāo)毀,再次使用時(shí),它會(huì)被重新創(chuàng)建,它之前保存下來(lái)的所有狀態(tài)都會(huì)被重置。
要預(yù)防這個(gè)問(wèn)題,我們不能用 Applicaiton 對(duì)象來(lái)傳遞數(shù)據(jù),而是要:
1) 通過(guò)傳統(tǒng)的 intent 來(lái)顯示傳遞數(shù)據(jù)(將 Parcelable 或 Serializable 對(duì)象放入intent / Bundle.Parcelable 性能比 Serializable 快一個(gè)量級(jí),但是代碼實(shí)現(xiàn)要復(fù)雜一些)。
2) 重寫(xiě) onSaveInstanceState()以及 onRestoreInstanceState()方法,確保進(jìn)程被殺掉時(shí)保存了必須的應(yīng)用狀態(tài),從而在重新打開(kāi)時(shí)可以正確恢復(fù)現(xiàn)場(chǎng)。
3) 使用合適的方式將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)或硬盤(pán)。
4) 總是做判空保護(hù)和處理。
- 在 Android 中使用單例還有哪些需要注意的地方
單例在 Android 中的生命周期等于應(yīng)用的生命周期,所以要特別小心它持有的對(duì)象是否會(huì)造成內(nèi)存泄露,所以最好僅傳遞給單例 Application Context。
附錄
雙重檢查鎖定(DCL)單例在JDK 1.5 之前版本失效原因解釋
在高并發(fā)環(huán)境,JDK 1.4 及更早版本下,雙重鎖定偶爾會(huì)失敗。其根本原因是,Java 中 new 一個(gè)對(duì)象并不是原子操作,編譯時(shí) singleton = new Singleton ; 語(yǔ)句會(huì)被轉(zhuǎn)成多條匯編指令,大致做了3件事情:
1) 給 Singleton 類(lèi)的實(shí)例分配內(nèi)存空間;
2) 調(diào)用私有的構(gòu)造函數(shù) Singleton(),初始化成員變量;
3) 將 singleton 對(duì)象指向分配的內(nèi)存(執(zhí)行玩此操作 singleton 就不是 null 了);
由于 Java 編譯器允許處理器亂序執(zhí)行,以及 JDK 1.5 之前的舊的 Java 內(nèi)存模型中 Cache、寄存器到主內(nèi)存回寫(xiě)順序的規(guī)定,上面步驟 2) 和 3) 的執(zhí)行順序是無(wú)法確定的,可能是 1) → 2) → 3) 也可能是 1) → 3) → 2) 。如果是后一種情況,在線程 A 執(zhí)行完步驟 3) 但還沒(méi)完成 2) 之前,被切換到線程 B 上,此時(shí)線程 B 對(duì) singleton 第1次判空結(jié)果為 false,直接取走了 singleton使用,但是構(gòu)造函數(shù)卻還沒(méi)有完成所有的初始化工作,就會(huì)出錯(cuò),也就是 DCL 失效問(wèn)題。
在 JDK 1.5的版本中具體化了 volatile 關(guān)鍵字,將其加在對(duì)象前就可以保證每次都是從主內(nèi)存中讀取對(duì)象,從而修復(fù)了 DCL 失效問(wèn)題。當(dāng)然,volatile 或多或少還是會(huì)影響到一些性能,但比起得到錯(cuò)誤的結(jié)果,犧牲這點(diǎn)性能還是值得的。
參考資料
[1] opalli. 那些年,我們一起寫(xiě)過(guò)的 “單例模式”[EB/OL]. [2017-03-09]. http://mp.weixin.qq.com/s/wEK3UcHjaHz1x-iXoW4_VQ.
[2] Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides. 設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)[M]. 李英軍等譯.北京:機(jī)械工業(yè)出版社,2009.
[3] 程杰. 大話(huà)設(shè)計(jì)模式[M]. 北京 : 清華大學(xué)出版社 , 2007.