定義
確保某個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例。
應(yīng)用場景
確保某個類有且只有一個對象的場景,避免產(chǎn)生多個對象消耗過多資源,或者某種類型的對象只應(yīng)該有且只有一個。例如,創(chuàng)建一個對象需要消耗的資源過多,如果要訪問IO和數(shù)據(jù)庫等資源,這時就要考慮使用單例模式。
關(guān)鍵點
- 構(gòu)造函數(shù)不對外開放。
- 通過靜態(tài)方法或枚舉返回單例類對象。
- 確保單例類的對象有且只有一個,特別是在多線程環(huán)境下。
- 確保單例類的對象在反序列化時不會重新構(gòu)造對象。
單例模式實現(xiàn)方式
實現(xiàn)方式比較多,但是都有各自的優(yōu)缺點。
餓漢模式
餓漢模式是在類加載時就創(chuàng)建好單例類實例。
實現(xiàn)
public class HungerSingleton {
private static HungerSingleton mInstance = new HungerSingleton();
private HungerSingleton() {
}
public static HungerSingleton getInstance() {
return mInstance;
}
}
instance是靜態(tài)對象,在聲明的時候就已經(jīng)初始化了,保證了對象的唯一性。
優(yōu)點:
- 簡單明了,不需要關(guān)心線程同步的問題。
- 獲取對象比較快,不需要做其他任何工作。
缺點:
- 類加載時因為需要初始化對象,所以比較慢。
- 可能會產(chǎn)生很多多余無用的對象。
適用場景
如果單例模式實例在系統(tǒng)中經(jīng)常會被用到,選擇此方式實現(xiàn)單例比較合適。但是如果對象初始化工作復(fù)雜且在程序運(yùn)行過程中不一定會使用到此單例對象,那餓漢模式就不太適合了。
懶漢模式
懶漢模式不同于餓漢模式的是,它是在第一次調(diào)用getInstance()時才初始化單例對象。
實現(xiàn)
public class LazySingleton {
private static LazySingleton mInstance;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (mInstance == null) {
mInstance = new LazySingleton();
}
return mInstance;
}
}
給getInstance()加了synchronized關(guān)鍵字,這樣就能夠保證不會有兩個線程同時去訪問這個方法創(chuàng)建重復(fù)對象了,將getInstance()變?yōu)橥椒椒ūWC了單例對象的唯一性。但是在初始化之后獲取對象時都不可避免的會造成同步開銷。
優(yōu)點
- 節(jié)約資源,在使用到時才會初始化單例對象。
缺點
- 第一次調(diào)用反應(yīng)稍慢,需要加載的時間。
- 每次調(diào)用都會進(jìn)行同步,造成不必要的同步開銷。
DCL模式
DCL優(yōu)點是既能在使用使才初始化對象,又能保證線程安全,且在初始化之后再調(diào)用getInstance()不進(jìn)行同步鎖。
實現(xiàn)
public class DCLSingleton {
private static DCLSingleton mInstance = null;
private DCLSingleton() {
}
public static DCLSingleton getInstance() {
if (mInstance == null) {
synchronized (DCLSingleton.class) {
if (mInstance == null) {
mInstance = new DCLSingleton();
}
}
}
return mInstance;
}
}
外層對instance的判空避免了多余的同步工作,只有在初始化對象時才需要同步。
內(nèi)層的instance的判空保證了對象的唯一。
DCL失效問題
這種方法暫時還是不安全的,并不能保證一定單例,還是有幾率會失敗的。
主要原因是new一個對象這個操作并不是原子性的,它會被編譯為三條匯編指令,分為三個步驟。但是由于JVM的指令重排,這三個步驟的后兩個步驟順序不定。
- 分配內(nèi)存空間。
- 初始化內(nèi)存空間。
- 引用變量指向這塊內(nèi)存空間。
指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率。指令重排序包括編譯器重排序和運(yùn)行時重排序。JVM規(guī)范規(guī)定,指令重排序可以在不影響單線程程序執(zhí)行結(jié)果前提下進(jìn)行。
指令重排并沒有考慮多線程的情況,所以可能會出現(xiàn)下面的場景。
| Thread 1 | Thread 2 |
|---|---|
| 外層instance判空:yes | - |
| 進(jìn)入同步代碼塊 | - |
| 內(nèi)層instance判空:yes | - |
| 為單例對象分配內(nèi)存空間 | - |
| instance指向這塊內(nèi)存空間 | - |
| - | 外層instance判空:no |
| - | 直接返回未初始化完畢的instance對象 |
| 初始化這塊內(nèi)存空間 | - |
這樣就會拿到一個未初始化完成的對象了,這就是DCL失效問題。
解決失效問題
在java5之后,具體化了volatile關(guān)鍵字,所以在java5之后,就可以用這個關(guān)鍵字來解決DCL失效問題了。
//使用 volatile修飾 instance
private volatile static DCLSingleton mInstance = null;
volatile防止指令重排序,禁止把new過程的指令與把引用賦值給變量的語句重排序,賦值只發(fā)生在new結(jié)束之后。這樣就不會出現(xiàn)上述的失效問題了。
volatile或多或少會影響性能,但是保證了DCL的正確性。
優(yōu)點
- 節(jié)約資源,在使用到時才會初始化單例對象。
缺點
- 第一次調(diào)用反應(yīng)稍慢,需要加載的時間。
- 會有一定的機(jī)率發(fā)生問題。在jdk5之后可以使用volatile保證正確性,volatile或多或少會影響性能。
靜態(tài)內(nèi)部類
同樣也是在第一次調(diào)用getInstance()才會初始化單例對象。
實現(xiàn)
第一次調(diào)用getInstance()時會導(dǎo)致虛擬機(jī)加載SingletonHolder類,初始化單例對象,這種方法不僅保證了線程安全,單例對象的唯一性,也延遲了單例對象的實例化,實現(xiàn)起來還非常簡單。
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.mInstance;
}
private static class SingletonHolder {
private static final StaticInnerSingleton mInstance = new StaticInnerSingleton();
}
}
在類的初始化期間,JVM會去獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化,從而保證了單例唯一。
枚舉單例
這可能是最簡單的單例實現(xiàn)了。枚舉和普通類一樣可以擁有字段和方法,并且默認(rèn)枚舉實例創(chuàng)建是線程安全的,在仍和時刻都是單例的,并且哪怕是支持反序列化,也不會生成新的單例對象。反序列化不同于普通類,枚舉是根據(jù)名字去查找存在的對象,而不是重新創(chuàng)建對象。
實現(xiàn)
public enum EnumSingleton {
INSTANCE;
public int a = 2;
public int aaa() {
return a;
}
}
枚舉在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結(jié)果中,反序列化的時候則是通****過java.lang.Enum的valueOf方法來根據(jù)名字查找枚舉對象。
支持反序列化
而上面幾種方法,如果需要保證完全的單例,還需要去增加readResolve(),需要做如下修改,拿支持序列化的餓漢模式舉例:
public class HungerSingleton implements Serializable {
private static final long serialVersionUID = 1L;
//...
private Object readResolve() throws ObjectStreamException {
return mInstance;
}
直接將單例對象返回而不是重新創(chuàng)建新對象。
集合實現(xiàn)
實現(xiàn)
通過一個map來保存所有的單例對象,這種方式換了一種思路,從單例自身轉(zhuǎn)移到單例保存方式上,提供get()、register()來提供單例和獲取單例。
public class MapSingletonManager {
private static Map<String, Object> objMap = new HashMap<>();
private MapSingletonManager() {
}
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);
}
}
這種方法降低了耦合,不會像之前的方法一樣將單例邏輯糅雜在類中。但是這種方式需要自己去創(chuàng)建單例對象,并且并不能保證堆中只有一個單例對象,只能保證在使用時,從map中提取出來的是同一個單例對象。
小結(jié)
書上推薦DCL和靜態(tài)內(nèi)部類的方式實現(xiàn)單例,最后總結(jié)一下單例模式的優(yōu)缺點。
優(yōu)點
- 減少了內(nèi)存開銷。
- 降低了系統(tǒng)性能開銷。
- 避免對資源的多重占用。
- 可以設(shè)置全局訪問點,優(yōu)化和共享資源訪問。
缺點
- 拓展困難,一般沒有接口。
- 單例對象如果持有Context,就容易發(fā)生內(nèi)存泄漏。
如果隨便地傳入一個Activity的Context,那么無論這個Activity是否還需要,它都因為Context被單例對象持有,所以Activity無法回收,只要項目活著,這個Activity就活著,所以就造成了內(nèi)存泄漏。所以如果單例中需要Context,就最好傳遞Application的Context,因為Application的生命周期和應(yīng)用一樣長。