簡介
單例模式是應(yīng)用最廣的模式之一,它是為了確保某一個類在一個java虛擬機(進程)中有且只有一個實例存在.
帶來的效益:
- 能夠?qū)崿F(xiàn)資源共享,避免由于資源操作時導致的性能或損耗.
- 能夠?qū)崿F(xiàn)資源調(diào)度,方便資源之間的互相通信.
- 控制實例產(chǎn)生的數(shù)量,達到節(jié)約資源的目的.
缺陷 :
- 擴展性差,單例一般沒有接口,要擴展只能修改單例類的代碼.
- 避免在單例中持有生命周期比單例對象短的引用,容易引起內(nèi)存泄漏.如Android中的Context對象,需要使用 Application Context代替.
下面介紹單例的七種經(jīng)典實現(xiàn)方法.
餓漢模式
public class Singleton {
// 靜態(tài)變量初始化, 由于靜態(tài)變量在類加載過程中,就會被初始化,且類加載又jvm保證線程安全.
// 所以這種方式 是線程安全的
private final static Singleton mInstance = new Singleton();
// 構(gòu)造函數(shù)私有化
private Singleton() {
// 判斷存在則拋出異常, 為了避免反射調(diào)用,產(chǎn)生多個實例
if (mInstance != null)
throw new RuntimeException("instance exist");
}
public static Singleton getInstance() {
return mInstance;
}
}
餓漢模式將變量聲明為靜態(tài),將在Singleton類被加載的時候,在cinit
階段進行創(chuàng)建對象,并且是線程安全
的, 類加載過程由JVM來保證線程安全.
餓漢模式能否達到懶加載
我們知道餓漢模式的對象實例是在類加載(初始化階段)的過程就被創(chuàng)建了,并且并不是所有的類都是在程序啟動的時候就加載進內(nèi)存,那么一個類在什么情況下會被加載或者初始化呢?
這在虛擬機規(guī)范中是有嚴格規(guī)定的员凝,虛擬機規(guī)范指明 有且只有 五種情況必須立即對類進行初始化:
1 ) 遇到new
阻课、getstatic
、putstatic
或invokestatic
這四條字節(jié)碼指令
2 ) 使用java.lang.reflect
包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化典鸡,則需要先觸發(fā)其初始化。
3 ) 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化脓豪。
4 ) 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類忌卤,虛擬機會先初始化這個主類扫夜。
5 ) 當使用jdk1.7動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle
實例最后的解析結(jié)果,REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄驰徊,并且這個方法句柄所對應(yīng)的類沒有進行初始化笤闯,則需要先出觸發(fā)其初始化。
--------- 引用自 <<深入理解Java虛擬機:JVM高級特性與最佳實踐>>
在這五種情況中,其中2,3,4,5 在單例模式中,幾乎不會遇到,這里暫不討論.
我們來看第一種情況,提到的指令分別對應(yīng)以下操作:
- 外部使用
new
創(chuàng)建該類的對象實例 - 類中的靜態(tài)變量被外部讀取或者設(shè)置
- 外部調(diào)用了 該類的靜態(tài)方法
其中1, 我們把構(gòu)造函數(shù)設(shè)置為 私有(需要提防 反射和反序列化)
,進本上不會產(chǎn)生問題.
對于2, 我們盡量要避免把變量(除單例變量外)
設(shè)為靜態(tài)且非私有(除非你確定在做什么,不然很可能出現(xiàn)內(nèi)存浪費或者內(nèi)存泄漏,畢竟靜態(tài)變量生命周期和程序一樣長
).如果外部調(diào)用這樣的類變量,將會觸發(fā)改類初始化.
注釋: 靜態(tài)常量(final static修飾基礎(chǔ)類型變量)的調(diào)用不會觸發(fā)類的加載, 該常量會被加入被調(diào)用類的常量池中
對于3, 我們單例如果提供靜態(tài)方法供外部使用,該靜態(tài)方法被調(diào)用時,將也會進行單例類初始化.但是 靜態(tài)方法,只能調(diào)用static
變量,參數(shù)變量,以及局部變量,而靜態(tài)變量在單例中基本上只有 單例本身會聲明為靜態(tài)變量, 總結(jié)起來就是, 這個靜態(tài)方法基本只能達到 工具方法的作用,最好不要聲明在單例中.
結(jié)論: 餓漢模式不能嚴格上實現(xiàn)懶加載,除非嚴格按照要求,不在單例中申明無關(guān)的靜態(tài)變量和靜態(tài)方法,將也能達到 懶加載的效果.
懶漢模式(線程不安全)
public class Singleton {
private static Singleton sInstance = null;
private Singleton() {
// 防止反射調(diào)用,被創(chuàng)建出多個實例
if (sInstance != null)
throw new RuntimeException("instance exist");
}
// 調(diào)用時創(chuàng)建
public static Singleton getInstance() {
if (sInstance == null)
sInstance = new Singleton();
return sInstance;
}
}
這種方式能實現(xiàn)懶加載
的目的,并且沒有加鎖操作,因此線程不安全
,減少了資源的消耗.
在單線程模型下,推薦這種方式的單例, 在多線程模式下 強烈不推薦.
懶漢模式(線程安全)
public class Singleton {
private static Singleton sInstance = null;
private Singleton() {
// 防止反射調(diào)用,被創(chuàng)建出多個實例
if (sInstance != null)
throw new RuntimeException("instance exist");
}
// 調(diào)用時創(chuàng)建
public synchronized static Singleton getInstance() {
if (sInstance == null)
sInstance = new Singleton();
return sInstance;
}
}
這種方式這種方式能夠達到 懶加載
和 線程安全
,但是 它鎖住了 整個getInstance()
方法,
對于讀的操作if (sInstance == null)
,也進行了加鎖,這樣對性能有一定的影響.
因此,不大推薦這種方式.
DCL 雙重檢查鎖模式
public class Singleton {
// 聲明為 volatile 是為了避免在多線程中,new對象時,指令重排,
// 造成對象未創(chuàng)建,而判斷為非空的情況
private volatile static Singleton sInstance = null;
private Singleton() {
// 防止反射調(diào)用,被創(chuàng)建出多個實例
if (sInstance != null)
throw new RuntimeException("instance exist");
}
public synchronized static Singleton getInstance() {
// 不加鎖,判斷是否為空, 在鎖競爭的情況下,提高性能
if (sInstance == null) {
// 只有當為空的時候,加鎖創(chuàng)建
synchronized (Singleton.class) {
if (sInstance == null)
sInstance = new Singleton();
}
}
return sInstance;
}
}
這種方式這種方式能夠達到 懶加載
和 線程安全
, 并且沒有懶漢模式
模式的缺點.它只對'寫'(即new
對象)操作進行加鎖,判斷是否為空時,線程無需等待.
這里需要注意,
sInstance
必須聲明為volatile
,不然達不到線程安全. 對象的創(chuàng)建可以拆分為 三條指令,如果對其指令重排就可能出現(xiàn)線程不安全的情況. 具體可以參考筆者的另一篇文章 深入理解 java volatile
因此,比較推薦這種寫法.
靜態(tài)內(nèi)部類模式
public class Singleton {
private Singleton() {
// 防止反射調(diào)用,被創(chuàng)建出多個實例
if (SingletonHolder.sInstance != null)
throw new RuntimeException("instance exist");
}
// 當該靜態(tài)方法被第一次調(diào)用時,SingletonHolder類被加載到內(nèi)存,
// 此時,其sInstance變量將會被創(chuàng)建,類加載由jvm保證線程安全
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
// 類加載時初始化,達到懶加載的目的.
// 調(diào)用時才被創(chuàng)建
private static class SingletonHolder {
private final static Singleton sInstance = new Singleton();
}
}
這種方式這種方式能夠達到 懶加載
和 線程安全
.
能夠?qū)崿F(xiàn)懶加載,是因為,不管
Singleton
中存不存在其他靜態(tài)變量或者靜態(tài)方法,都不會影響到 內(nèi)部靜態(tài)類SingletonHolder
, 只有當getInstance()
方法調(diào)用時,內(nèi)部靜態(tài)類才會被加載,而類加載時,單例被創(chuàng)建實例化. (請對比餓漢模式)
餓漢模式與靜態(tài)內(nèi)部類模式對比
餓漢模式
要對類進行約束也能達到懶加載目的. (不適用多余的靜態(tài)變量和靜態(tài)方法)
.
靜態(tài)內(nèi)部類模式
不需要進行約束就能達到懶加載目的. 但是需要消耗一個內(nèi)部類的資源來達到目的.(代價很小)
權(quán)衡兩者, 推薦使用靜態(tài)內(nèi)部類方式.
枚舉模式
public enum Singleton {
INSTANCE;
public void method() {
// todo ...
}
}
上述的單例方式都有兩個致命的缺點, 不能完全保證單例在jvm中保持唯一性.
- 反射創(chuàng)建單例對象
解決方案 : 在構(gòu)造上述中判斷,當多于一個實例時,再調(diào)用構(gòu)造函數(shù),直接報錯.
- 反序列化時創(chuàng)建對象
解決方案 : 使用readResolve()方法來避免此事發(fā)生.
這兩種缺點雖然都有方式解決,但是不免有些繁瑣.
枚舉類天生有這些特性.而且實現(xiàn)單例相當簡單.
關(guān)于枚舉類型,能夠?qū)崿F(xiàn) 懶加載
,線程安全
,以及確保單例在jvm中保持唯一性
.
請參考筆者的另一篇文章 java 枚舉(enum) 全面解讀
因此, 強力推薦
使用這種方式創(chuàng)建單例.
但是由于枚舉的使用時,枚舉類的裝載和初始化時會有更多的時間和空間的成本, 它的實現(xiàn)比其他方式需要更多的內(nèi)存空間,所以在
Android
這種受資源約束的設(shè)備中盡量避免使用枚舉單例
總結(jié)
- 在單線程下,建議使用
懶漢模式(線程不安全版)
- 多線程,且資源受限(
Android
),建議使用DCL
和靜態(tài)內(nèi)部類
版本 - 其他情況,建議使用
枚舉
方法