單例模式作為23種設(shè)計模式中最常用的一種,也是面試中的???,但是很少有人能把每種實現(xiàn)方式的不同講清楚的,今天我們就來探討下它。
1、單例模式的作用
單例模式主要用于解決多次調(diào)用的地方都是用的同一個實例,避免一個應(yīng)用中存在多個該對象的實例,常見的實現(xiàn)手段就是不允許外部創(chuàng)建對象,將構(gòu)造方法私有化,自己提供靜態(tài)方法將對象的實例暴露出去。
2、常見的幾種單例模式的實現(xiàn)
2.1 懶漢、餓漢、變種餓漢(靜態(tài)代碼塊實現(xiàn))模式
這兩種模式在實現(xiàn)上都有一定的缺陷,懶漢模式要實現(xiàn)多線程安全就需要對getInstance()方法上加上synchronize 關(guān)鍵字,效率很低,餓漢模式雖然一開始就創(chuàng)建了實例,但是在類加載的時候就創(chuàng)建了對象,不能實現(xiàn)懶加載。
懶漢:
/**
* Created by jiangcheng on 2017/9/28.
*/
public class Singleton {
private static Singleton sInstance;
// 私有構(gòu)造方法
private Singleton() {}
public static synchronized Singleton getInstance () {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
2.2 雙重檢測
由于懶漢模式是對方法進(jìn)行加鎖的,所以當(dāng)多個線程同時訪問該方法時,效率很低,synchronized修飾的同步方法比一般方法要慢很多。
雙重檢測:
public class Singleton {
private static Singleton sInstance;
private Singleton() {}
public static Singleton getInstance () {
if (sInstance == null) { // 1
synchronized (Singleton.class) {
if (sInstance == null) { // 2
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
我們來分析下,雙重檢測的原理,可以看到上面代碼1處在同步代碼塊外多了一層instance為空的判斷。由于單例對象只需要創(chuàng)建一次,如果后面再次調(diào)用getInstance()只需要直接返回單例對象。因此,大部分情況下,調(diào)用getInstance()都不會執(zhí)行到同步代碼塊,從而提高了程序性能。不過還需要考慮一種情況,假如兩個線程A、B,A執(zhí)行了if (instance == null)語句,它會認(rèn)為單例對象沒有創(chuàng)建,此時線程切到B也執(zhí)行了同樣的語句,B也認(rèn)為單例對象沒有創(chuàng)建,然后兩個線程依次執(zhí)行同步代碼塊,并分別創(chuàng)建了一個單例對象。為了解決這個問題,還需要在同步代碼塊中增加if (instance == null)語句,也就是上面看到的代碼2。
- 這種實現(xiàn)就真的完美么?
由于java平臺的指定優(yōu)化重排后的無序性,會導(dǎo)致初始化Singleton和將對象地址賦給instance字段的順序是不確定的。在某個線程創(chuàng)建單例對象時,在構(gòu)造方法被調(diào)用之前,就為該對象分配了內(nèi)存空間并將對象的字段設(shè)置為默認(rèn)值。此時就可以將分配的內(nèi)存地址賦值給instance字段了,然而該對象可能還沒有初始化。若緊接著另外一個線程來調(diào)用getInstance,取到的就是狀態(tài)不正確的對象,程序就會出錯,雙重檢測就會失效。
不過JDK 1.5后 Java中提供了關(guān)鍵字 volatile。 volatile的一個語義是禁止指令重排序優(yōu)化,他定義的變量在多線程操作中,是對所有線程可見的,也就保證了instance變量被賦值的時候?qū)ο笠呀?jīng)是初始化過的,從而避免了上面說到的問題。
private static volatile Singleton sInstance; // volatile 關(guān)鍵字的使用
2.3 靜態(tài)內(nèi)部類(推薦)
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static Singleton sInstance = new Singleton();
}
public static Singleton getInstance () {
return SingletonHolder.sInstance;
}
}
這種方式利用了Java在類加載的時候只允許一個線程加載,它是在內(nèi)部類里面去創(chuàng)建對象實例。這樣的話,只要應(yīng)用中不使用內(nèi)部類,JVM就不會去加載這個單例類,也就不會創(chuàng)建單例對象,從而實現(xiàn)懶漢式的延遲加載。也就是說這種方式可以同時保證延遲加載和線程安全。這種方式比較容易看的懂,也很簡潔,所以推薦改種實現(xiàn)方式。
2.4 枚舉
public enum Singleton {
instance;
public void whateverMethod() {
}
}
由于前面幾種都有兩個共同的缺點:
- 需要額外的工作來實現(xiàn)序列化,否則每次反序列化一個序列化的對象時都會創(chuàng)建一個新的實例。
- 可以使用反射強(qiáng)行調(diào)用私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個實例的時候拋異常)。
而枚舉類很好的解決了這兩個問題,使用枚舉除了線程安全和防止反射調(diào)用構(gòu)造器之外,還提供了自動序列化機(jī)制,防止反序列化的時候創(chuàng)建新的對象。因此,《Effective Java》作者推薦使用的方法。不過,在實際工作中,很少看見有人這么寫。
枚舉在Java中是很特殊的存在,可以參考Java枚舉enum及其應(yīng)用
3、總結(jié)
上面幾種單例模式的實現(xiàn)都各有優(yōu)缺點,適用于不同的場景,不過雙重檢測和靜態(tài)內(nèi)部類能解決工作中大部分的問題,枚舉雖然很有特色、很完美,但是工作中用的還是比較少的。我的建議是使用靜態(tài)內(nèi)部類,簡單易懂。