單例,真了解嗎?

前段時間參加了幾場面試,正式面試之前都需要你先做一份筆試題,我發(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)了延遲初始化。

略陳固陋,如有不當之處,歡迎各位看官批評指正!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 從三月份找實習到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,810評論 11 349
  • 一個簡單的單例示例 單例模式可能是大家經(jīng)常接觸和使用的一個設(shè)計模式,你可能會這么寫 publicclassUnsa...
    Martin說閱讀 2,399評論 0 6
  • 1.單例模式概述 (1)引言 單例模式是應(yīng)用最廣的模式之一,也是23種設(shè)計模式中最基本的一個。本文旨在總結(jié)通過Ja...
    曹豐斌閱讀 3,072評論 6 47
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,728評論 18 399
  • 版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載 PS:轉(zhuǎn)載請注明出處作者: TigerChain地址: htt...
    TigerChain閱讀 1,390評論 0 3

友情鏈接更多精彩內(nèi)容