前段時間參加了幾場面試,正式面試之前都需要你先做一份筆試題,我發(fā)現(xiàn)有三家公司的筆試題都出現(xiàn)了同一個題目:寫出一個你認為最優(yōu)的單例實現(xiàn)方式?
單例模式,23種設(shè)計模式中很常見的一種,意思就是一個類只能有一個對象實例。此題一出,腦子里是不是立馬回想起當年課堂上講的兩種方式“懶漢式”和“餓漢式”,是不是心中暗喜面試有戲了,但只有這兩種嗎?這兩種是最優(yōu)的嗎?如果你真的只知道這兩種,只能送你一句話:城市套路深,還是回農(nóng)村。
下面就介紹下幾種單例實現(xiàn)方式:
1、懶漢式(非線程安全)
public class SingletonTest {
private static SingletonTest instance;
//注意構(gòu)造方法的訪問控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
if(instance == null){
instance = new SingletonTest();
}
return instance;
}
}
這種懶加載方式缺點很明顯,多個線程執(zhí)行時,就出問題了,可以簡單測試下。
public class SingletonMain {
public static void main(String[] args) {
Test test = new Test();
Thread thread1 = new Thread(test, "線程1");
Thread thread2 = new Thread(test, "線程2");
Thread thread3 = new Thread(test, "線程3");
thread1.start();
thread2.start();
thread3.start();
}
public static class Test implements Runnable{
@Override
public void run() {
SingletonTest instance = SingletonTest.getInstance();
System.out.println(instance.hashCode());
}
}
}
執(zhí)行結(jié)果:

可以看出,當啟動三個線程后,所獲取的對象實例有三個不同的哈希碼(不一定每次都是三個不一樣的,如果是同一個對象實例,哈希碼必定一樣),獲取了三個不同的對象,所以這種懶加載方式已經(jīng)失去了單例的意義。
2、懶漢式(線程安全)
與第1種相比,區(qū)別只是在getInstance()前加了synchronized,鎖定對象是整個類。
public class SingletonTest {
private static SingletonTest instance;
//注意構(gòu)造方法的訪問控制符是private
private SingletonTest(){}
public static synchronized SingletonTest getInstance(){
if(instance == null){
instance = new SingletonTest();
}
return instance;
}
}
繼續(xù)執(zhí)行上面的小測試,可以看到獲取的哈希碼都相同(還可以再多創(chuàng)建幾個線程),可以看出此方法在多線程下可以正常工作,只是效率較低,99%的情況下不用同步。
3、餓漢式
public class SingletonTest {
private static SingletonTest instance = new SingletonTest();
//注意構(gòu)造方法的訪問控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
return instance;
}
}
這種方式在多線程情況下亦能正常工作,調(diào)用對象的時候不用創(chuàng)建,直接使用已經(jīng)創(chuàng)建好的對象,節(jié)省了時間,但是卻占用了空間,因instance在類裝載時就實例化。
4、靜態(tài)內(nèi)部類(推薦)
public class SingletonTest {
private static class SingletonHolder{
private static SingletonTest instance = new SingletonTest();
}
//注意構(gòu)造方法的訪問控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
return SingletonHolder.instance;
}
}
相比第2、3種方式,這種方式不僅是線程安全的,而且實現(xiàn)了延遲初始化。SingletonTest 被裝載了,instance不一定實例化,因為SingletonHolder類沒有被主動使用,只有顯示通過調(diào)用getInstance方法時,才會顯示裝載SingletonHolder類,從而實例化instance。
5、雙重檢查加鎖(DCL)
針對第2種懶漢式(線程安全)的實現(xiàn)方式的缺點,如果getInstance()方法被多個線程頻繁調(diào)用,勢必導(dǎo)致性能的下降。因此,出現(xiàn)了DCL這種方式,通過兩次檢查鎖定來降低同步的開銷。
public class SingletonTest { //1
private static SingletonTest instance; //2
//注意構(gòu)造方法的訪問控制符是private //3
private SingletonTest(){} //4
public static SingletonTest getInstance(){ //5
if(instance == null){ //6
synchronized (SingletonTest.class){ //7
if(instance == null){ //8
instance = new SingletonTest(); //9
}
}
}
return instance;
}
}
創(chuàng)建一個對象正常的套路應(yīng)該是:①分配對象的內(nèi)存空間;②初始化對象;③設(shè)置instance指向內(nèi)存空間。這種方式看似沒有問題,但如果在多線程環(huán)境下發(fā)生了重排序(重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種方式,但必須保證排序后程序的執(zhí)行結(jié)果不變,也就是as-if-serial語義),比如說執(zhí)行到第6行,發(fā)現(xiàn)instance不為null,但是instance可能還沒有完成初始化,就會訪問一個未初始化的對象,就有問題了。
如果發(fā)生了重排序,在單線程和多線程下的時序圖如下(波浪線上為單線程,波浪線下為多線程):

那如果禁止第2步和第3步重排序不就解決了嗎?只需要把instance聲明為volatile即可,實現(xiàn)線程安全的延遲初始化。(關(guān)于volatile特性,后續(xù)介紹)
public class SingletonTest {
private volatile static SingletonTest instance;
//注意構(gòu)造方法的訪問控制符是private
private SingletonTest(){}
public static SingletonTest getInstance(){
if(instance == null){
synchronized (SingletonTest.class){
if(instance == null){
instance = new SingletonTest();
}
}
}
return instance;
}
}
總結(jié)
本篇主要介紹了實現(xiàn)單例的幾種方式,只有第1種方式是線程非安全的,其余的幾種方式中,第2種效率低一點,第3種占用空間多一點,第5種主要是彌補第2種的缺點,比較推薦使用第4種方式,線程安全,并且實現(xiàn)了延遲初始化。
略陳固陋,如有不當之處,歡迎各位看官批評指正!