定義
確保某個(gè)類只有一個(gè)實(shí)例五鲫,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例轿钠。
應(yīng)用場(chǎng)景
確保某個(gè)類有且只有一個(gè)對(duì)象的場(chǎng)景,避免產(chǎn)生多個(gè)對(duì)象消耗過(guò)多資源涕滋,或者某種類型的對(duì)象只應(yīng)該有且只有一個(gè)睬辐。例如,創(chuàng)建一個(gè)對(duì)象需要消耗的資源過(guò)多宾肺,如果要訪問(wèn)IO和數(shù)據(jù)庫(kù)等資源溯饵,這時(shí)就要考慮使用單例模式。
關(guān)鍵點(diǎn)
- 構(gòu)造函數(shù)不對(duì)外開(kāi)放锨用。
- 通過(guò)靜態(tài)方法或枚舉返回單例類對(duì)象丰刊。
- 確保單例類的對(duì)象有且只有一個(gè),特別是在多線程環(huán)境下增拥。
- 確保單例類的對(duì)象在反序列化時(shí)不會(huì)重新構(gòu)造對(duì)象啄巧。
單例模式實(shí)現(xiàn)方式
實(shí)現(xiàn)方式比較多,但是都有各自的優(yōu)缺點(diǎn)掌栅。
餓漢模式
餓漢模式是在類加載時(shí)就創(chuàng)建好單例類實(shí)例秩仆。
實(shí)現(xiàn)
public class HungerSingleton {
private static HungerSingleton mInstance = new HungerSingleton();
private HungerSingleton() {
}
public static HungerSingleton getInstance() {
return mInstance;
}
}
instance是靜態(tài)對(duì)象,在聲明的時(shí)候就已經(jīng)初始化了猾封,保證了對(duì)象的唯一性澄耍。
優(yōu)點(diǎn):
- 簡(jiǎn)單明了,不需要關(guān)心線程同步的問(wèn)題忘衍。
- 獲取對(duì)象比較快逾苫,不需要做其他任何工作卿城。
缺點(diǎn):
- 類加載時(shí)因?yàn)樾枰跏蓟瘜?duì)象枚钓,所以比較慢。
- 可能會(huì)產(chǎn)生很多多余無(wú)用的對(duì)象瑟押。
適用場(chǎng)景
如果單例模式實(shí)例在系統(tǒng)中經(jīng)常會(huì)被用到搀捷,選擇此方式實(shí)現(xiàn)單例比較合適。但是如果對(duì)象初始化工作復(fù)雜且在程序運(yùn)行過(guò)程中不一定會(huì)使用到此單例對(duì)象多望,那餓漢模式就不太適合了嫩舟。
懶漢模式
懶漢模式不同于餓漢模式的是,它是在第一次調(diào)用getInstance()時(shí)才初始化單例對(duì)象怀偷。
實(shí)現(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)鍵字家厌,這樣就能夠保證不會(huì)有兩個(gè)線程同時(shí)去訪問(wèn)這個(gè)方法創(chuàng)建重復(fù)對(duì)象了,將getInstance()變?yōu)橥椒椒ūWC了單例對(duì)象的唯一性椎工。但是在初始化之后獲取對(duì)象時(shí)都不可避免的會(huì)造成同步開(kāi)銷饭于。
優(yōu)點(diǎn)
- 節(jié)約資源蜀踏,在使用到時(shí)才會(huì)初始化單例對(duì)象。
缺點(diǎn)
- 第一次調(diào)用反應(yīng)稍慢掰吕,需要加載的時(shí)間果覆。
- 每次調(diào)用都會(huì)進(jìn)行同步,造成不必要的同步開(kāi)銷殖熟。
DCL模式
DCL優(yōu)點(diǎn)是既能在使用使才初始化對(duì)象局待,又能保證線程安全,且在初始化之后再調(diào)用getInstance()不進(jìn)行同步鎖菱属。
實(shí)現(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;
}
}
外層對(duì)instance的判空避免了多余的同步工作钳榨,只有在初始化對(duì)象時(shí)才需要同步。
內(nèi)層的instance的判空保證了對(duì)象的唯一纽门。
DCL失效問(wèn)題
這種方法暫時(shí)還是不安全的重绷,并不能保證一定單例,還是有幾率會(huì)失敗的膜毁。
主要原因是new一個(gè)對(duì)象這個(gè)操作并不是原子性的昭卓,它會(huì)被編譯為三條匯編指令,分為三個(gè)步驟瘟滨。但是由于JVM的指令重排候醒,這三個(gè)步驟的后兩個(gè)步驟順序不定。
- 分配內(nèi)存空間杂瘸。
- 初始化內(nèi)存空間倒淫。
- 引用變量指向這塊內(nèi)存空間。
指令重排序是JVM為了優(yōu)化指令败玉,提高程序運(yùn)行效率敌土。指令重排序包括編譯器重排序和運(yùn)行時(shí)重排序。JVM規(guī)范規(guī)定运翼,指令重排序可以在不影響單線程程序執(zhí)行結(jié)果前提下進(jìn)行返干。
指令重排并沒(méi)有考慮多線程的情況,所以可能會(huì)出現(xiàn)下面的場(chǎng)景血淌。
Thread 1 | Thread 2 |
---|---|
外層instance判空:yes | - |
進(jìn)入同步代碼塊 | - |
內(nèi)層instance判空:yes | - |
為單例對(duì)象分配內(nèi)存空間 | - |
instance指向這塊內(nèi)存空間 | - |
- | 外層instance判空:no |
- | 直接返回未初始化完畢的instance對(duì)象 |
初始化這塊內(nèi)存空間 | - |
這樣就會(huì)拿到一個(gè)未初始化完成的對(duì)象了矩欠,這就是DCL失效問(wèn)題。
解決失效問(wèn)題
在java5之后悠夯,具體化了volatile關(guān)鍵字癌淮,所以在java5之后,就可以用這個(gè)關(guān)鍵字來(lái)解決DCL失效問(wèn)題了沦补。
//使用 volatile修飾 instance
private volatile static DCLSingleton mInstance = null;
volatile防止指令重排序乳蓄,禁止把new過(guò)程的指令與把引用賦值給變量的語(yǔ)句重排序,賦值只發(fā)生在new結(jié)束之后夕膀。這樣就不會(huì)出現(xiàn)上述的失效問(wèn)題了虚倒。
volatile或多或少會(huì)影響性能匣摘,但是保證了DCL的正確性。
優(yōu)點(diǎn)
- 節(jié)約資源裹刮,在使用到時(shí)才會(huì)初始化單例對(duì)象音榜。
缺點(diǎn)
- 第一次調(diào)用反應(yīng)稍慢,需要加載的時(shí)間捧弃。
- 會(huì)有一定的機(jī)率發(fā)生問(wèn)題赠叼。在jdk5之后可以使用volatile保證正確性,volatile或多或少會(huì)影響性能违霞。
靜態(tài)內(nèi)部類
同樣也是在第一次調(diào)用getInstance()才會(huì)初始化單例對(duì)象嘴办。
實(shí)現(xiàn)
第一次調(diào)用getInstance()時(shí)會(huì)導(dǎo)致虛擬機(jī)加載SingletonHolder類,初始化單例對(duì)象买鸽,這種方法不僅保證了線程安全涧郊,單例對(duì)象的唯一性,也延遲了單例對(duì)象的實(shí)例化眼五,實(shí)現(xiàn)起來(lái)還非常簡(jiǎ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會(huì)去獲取一個(gè)鎖看幼,這個(gè)鎖可以同步多個(gè)線程對(duì)同一個(gè)類的初始化批旺,從而保證了單例唯一。
枚舉單例
這可能是最簡(jiǎn)單的單例實(shí)現(xiàn)了诵姜。枚舉和普通類一樣可以擁有字段和方法汽煮,并且默認(rèn)枚舉實(shí)例創(chuàng)建是線程安全的,在仍和時(shí)刻都是單例的棚唆,并且哪怕是支持反序列化暇赤,也不會(huì)生成新的單例對(duì)象。反序列化不同于普通類宵凌,枚舉是根據(jù)名字去查找存在的對(duì)象鞋囊,而不是重新創(chuàng)建對(duì)象。
實(shí)現(xiàn)
public enum EnumSingleton {
INSTANCE;
public int a = 2;
public int aaa() {
return a;
}
}
枚舉在序列化的時(shí)候Java僅僅是將枚舉對(duì)象的name屬性輸出到結(jié)果中摆寄,反序列化的時(shí)候則是通****過(guò)java.lang.Enum的valueOf方法來(lái)根據(jù)名字查找枚舉對(duì)象失暴。
支持反序列化
而上面幾種方法坯门,如果需要保證完全的單例微饥,還需要去增加readResolve(),需要做如下修改古戴,拿支持序列化的餓漢模式舉例:
public class HungerSingleton implements Serializable {
private static final long serialVersionUID = 1L;
//...
private Object readResolve() throws ObjectStreamException {
return mInstance;
}
直接將單例對(duì)象返回而不是重新創(chuàng)建新對(duì)象欠橘。
集合實(shí)現(xiàn)
實(shí)現(xiàn)
通過(guò)一個(gè)map來(lái)保存所有的單例對(duì)象,這種方式換了一種思路现恼,從單例自身轉(zhuǎn)移到單例保存方式上肃续,提供get()黍檩、register()來(lái)提供單例和獲取單例。
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);
}
}
這種方法降低了耦合始锚,不會(huì)像之前的方法一樣將單例邏輯糅雜在類中刽酱。但是這種方式需要自己去創(chuàng)建單例對(duì)象,并且并不能保證堆中只有一個(gè)單例對(duì)象瞧捌,只能保證在使用時(shí)棵里,從map中提取出來(lái)的是同一個(gè)單例對(duì)象。
小結(jié)
書(shū)上推薦DCL和靜態(tài)內(nèi)部類的方式實(shí)現(xiàn)單例姐呐,最后總結(jié)一下單例模式的優(yōu)缺點(diǎn)殿怜。
優(yōu)點(diǎn)
- 減少了內(nèi)存開(kāi)銷。
- 降低了系統(tǒng)性能開(kāi)銷头谜。
- 避免對(duì)資源的多重占用。
- 可以設(shè)置全局訪問(wèn)點(diǎn)柱告,優(yōu)化和共享資源訪問(wèn)。
缺點(diǎn)
- 拓展困難笑陈,一般沒(méi)有接口。
- 單例對(duì)象如果持有Context新锈,就容易發(fā)生內(nèi)存泄漏。
如果隨便地傳入一個(gè)Activity的Context妹笆,那么無(wú)論這個(gè)Activity是否還需要,它都因?yàn)镃ontext被單例對(duì)象持有拳缠,所以Activity無(wú)法回收,只要項(xiàng)目活著窟坐,這個(gè)Activity就活著,所以就造成了內(nèi)存泄漏哲鸳。所以如果單例中需要Context臣疑,就最好傳遞Application的Context,因?yàn)锳pplication的生命周期和應(yīng)用一樣長(zhǎng)徙菠。