懶漢:
線程不安全:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
線程安全:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
這種寫法能夠在多線程中很好的工作,而且看起來它也具備lazy loading,但是,遺憾的是,效率很低,99%情況下不需要同步。
餓漢:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
這種方式基于classloder機制,避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時候初始化instance顯然沒有達(dá)到lazy loading的效果。
餓漢,變種:
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
表面上看起來差別挺大,其實跟上面的一種差不多,都是在類初始化即實例化instance。
靜態(tài)內(nèi)部類:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這種方式同樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟餓漢方式不同的是(很細(xì)微的差別):餓漢是只要Singleton類被裝載了,那么instance就會被實例化(沒有達(dá)到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過調(diào)用getInstance方法時,才會顯示裝載SingletonHolder類,從而實例化instance。想象一下,如果實例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時就實例化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被加載,那么這個時候?qū)嵗痠nstance顯然是不合適的。這個時候,這種方式相比餓漢模式就顯得很合理。
枚舉:
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
這種方式是< Effective Java>作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創(chuàng)建新的對象,可謂是很堅強的壁壘啊。不過,個人認(rèn)為由于1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,也很少看見有人這么寫過。
雙重校驗鎖:
public class Singleton {
private static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗一次?因為可能會有多個線程一起進(jìn)入同步塊外的 if,如果在同步塊內(nèi)不進(jìn)行二次檢驗的話就會生成多個實例了。
這段代碼看起來很完美,很可惜,它是有問題。主要在于instance = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
- 給 instance 分配內(nèi)存
- 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
- 將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
我們只需要將 instance 變量聲明成 volatile 就可以了。
public class Singleton {
private volatile static Singleton instance; //聲明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
使用volatile 的主要原因是:禁止指令重排序優(yōu)化。
總結(jié)
個人而言,一般情況下直接使用餓漢式就好了,如果明確要求要懶加載(lazy initialization)會傾向于使用靜態(tài)內(nèi)部類,如果涉及到反序列化創(chuàng)建對象時會試著使用枚舉的方式來實現(xiàn)單例。