單例模式

單例模式 Singleton
很多時(shí)候,我們對(duì)于某個(gè)類,是不需要有很多實(shí)例存在的。打個(gè)比方:1個(gè)教室里面有很多學(xué)生要喝水,但是只有1臺(tái)飲水機(jī),沒有必要給每個(gè)學(xué)生都安排1臺(tái)飲水機(jī)。
這個(gè)時(shí)候,我們就需要使用單例模式

一.餓漢式(最常用)

public class Singleton {
    //1.final修飾的常量一般字母大寫
    //2.自己內(nèi)部new出1個(gè)對(duì)象后 給構(gòu)造方法加private讓其他人無法new
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){};

    //3.private修飾的 需要有1個(gè)返回方法
    public static Singleton getInstance(){return INSTANCE;};


    public static void main(String[] args) {
        Singleton m1 = Singleton.getInstance();
        Singleton m2 = Singleton.getInstance();
        //驗(yàn)證 是不是 只會(huì)new出1個(gè)對(duì)象 如果地址相等 那么一定是同1個(gè)
        System.out.println(m1 == m2);

    }
}

餓漢式的優(yōu)點(diǎn):
1.類加載到內(nèi)存后,就實(shí)例化一個(gè)單例,而且JVM線程安全(因?yàn)镴VM保證每個(gè)class類 只會(huì)load到內(nèi)存一次 static變量是在類load后馬上進(jìn)行初始化的 所以它也只會(huì)初始化一次 多線程訪問也沒有關(guān)系)
2.簡單方便

private static final Singleton INSTANCE;
//可以把 static修飾符 變成static代碼塊
static {
  INSTANCE = new Singleton();
}
...其他不變

二.懶漢式
懶漢式 :臨到用時(shí)方恨少 用到時(shí)才創(chuàng)建

public class Singleton2 {
//注意我們這個(gè)對(duì)象不能 定義為final 因?yàn)槎x了final 就是常量要進(jìn)行static初始化(要么用static修飾 要么用static靜態(tài)代碼塊)
    private  static Singleton2 INSTANCE;
    private Singleton2(){

    }

    public static Singleton2 getInstance() {
        //懶漢式 :臨到用時(shí)方恨少 用到時(shí)才創(chuàng)建
        if(INSTANCE == null ){
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        //測試一下 是否獲取的是同一對(duì)象
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton2.getInstance().hashCode());
            }).start();
        }
    }
}

缺點(diǎn):多線程同時(shí)訪問時(shí) 線程不安全

    public static Singleton2 getInstance() {
        if(INSTANCE == null ){
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

假如第1個(gè)線程 調(diào)用了getInstance() 但是還沒有new Singleton2()
此時(shí)另外一個(gè)線程來了 它也會(huì)判斷if 有沒有INSTANCE 它此時(shí)是null的 那么第二個(gè)線程就會(huì)new一個(gè)Singletion2() 而第一個(gè)線程也繼續(xù)執(zhí)行 那么這個(gè)INSTANCE在兩個(gè)線程中 已經(jīng)不是同一個(gè)實(shí)例了

我們可以做個(gè)實(shí)驗(yàn) 在INSTACE = new Singleton2()之前 讓線程sleep一會(huì)兒

if(INSTANCE == null ){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
hashcode不同 所以new了不同的對(duì)象出來

三.加鎖版
為了解決懶漢式線程不安全的問題,我們可以在調(diào)用getInstace()前,給方法加上鎖

public class Singleton3 {
    private static Singleton3 INSTANCE;
    private Singleton3(){
    }

    public static synchronized Singleton3 getInstance(){
        if(INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton3.getInstance().hashCode());
            }).start();
        }
    }
}

缺點(diǎn):
1.內(nèi)存中的對(duì)象 大得多比原來的
2.用的時(shí)候要看 有沒有加鎖,有沒有申請(qǐng)到這把鎖 效率降低

四.1個(gè)錯(cuò)誤的單例模式
想減少鎖住的范圍,提高效率,但是卻讓線程不安全

public class Singleton4 {
    private static Singleton4 INSTANCE;
    private Singleton4(){
        
    }
    
    public  static Singleton4 getInstance() {
        if(INSTANCE == null ){
            synchronized (Singleton4.class) {
                INSTANCE = new Singleton4();
            }
            
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
              new Thread(()->{
                  System.out.println(Singleton4.getInstance().hashCode());
              }).start();  
        }
    }
}

這種方法不能做到只new出來1個(gè)實(shí)例:因?yàn)楫?dāng)?shù)?個(gè)線程 執(zhí)行到 synchronized之前時(shí),第2個(gè)線程 往下執(zhí)行完了,申請(qǐng)到了鎖,并且new出來對(duì)象Singleton4;然后第2個(gè)線程釋放鎖,第1個(gè)進(jìn)程進(jìn)入synchronized繼續(xù)執(zhí)行,它又new出來了1個(gè)Singleton4對(duì)象
本質(zhì)原因:就是因?yàn)?它對(duì)象的判空if(INSTANCE == null) 沒有和創(chuàng)建對(duì)象INSTANCE = new Singleton4(); 同時(shí)操作 只要有1個(gè)時(shí)間間隙 就會(huì)出現(xiàn) 不同的對(duì)象 就會(huì)線程不安全 必須在INSTANCE==null的前提條件 去new Singleton4才可以

五.雙重判斷/檢查法
我們?cè)趎ew Singleton前 我們?cè)倥锌?次 就行了

public class Singleton5 {
    private static Singleton5 INSTANCE;
    private Singleton5(){

    }
    public static Singleton5 getInstance(){
        if(INSTANCE == null){
            synchronized (Singleton5.class){
                //雙重檢查
                if(INSTANCE == null ){
                    INSTANCE = new Singleton5();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton4.getInstance().hashCode());
            }).start();
        }
    }
}

為什么要判斷兩次?第一次可以不可以去掉?

if(INSTANCE == null){
            synchronized (Singleton5.class){

其實(shí)有必要 第一次的判斷 因?yàn)檫@樣會(huì)省很多事 不然很多情況都要被上鎖 有很多情況 一旦INSTANCE被初始化后 進(jìn)來看到不為空后 它就不會(huì)繼續(xù)執(zhí)行下去了 提高效率

六.靜態(tài)內(nèi)部類方法

public class Singleton6 {
    //給構(gòu)造方法加上private是單例模式的標(biāo)配了
    private Singleton6(){
    }

    //加載外部類時(shí)不會(huì)加載內(nèi)部類,這樣可以實(shí)現(xiàn)懶加載
    private static class Singleton6Holder {
        //只有它的內(nèi)部類在內(nèi)部 才可以訪問 外部類訪問不了 
        private final static Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Singleton6Holder.INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton6.getInstance().hashCode());
            }).start();
        }
    }
}

加載外部類時(shí)不會(huì)加載內(nèi)部類,這樣可以實(shí)現(xiàn)懶加載,只有當(dāng)我們調(diào)用 getInstance()的時(shí)候 才會(huì)被加載,它是線程安全的,因?yàn)镴VM只加載class一次
這比上面幾種都好

七.枚舉類(Java創(chuàng)始人的寫法)

public enum Singleton7 {
    INSTANCE;

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton7.INSTANCE.hashCode());
            }).start();
        }
    }
}

這樣不僅可以解決線程不同步 還可以反序列化(因?yàn)閖ava 反射可以通過class文件 把整個(gè)class load到內(nèi)存 new出1個(gè)實(shí)例 前面的寫法 他都可以找到你的class文件 來new 1個(gè)INSTANCE出來 通過 反序列化的方式 而枚舉單例 不會(huì)被反序列化 它只是一個(gè)抽象類,因?yàn)槊杜e類 沒有構(gòu)造方法 就算你拿到他的class文件 也沒有辦法 構(gòu)造他的對(duì)象,返回的只是1個(gè)值 返回的是單例創(chuàng)建的1個(gè)對(duì)象)
缺點(diǎn):本來你是一個(gè)resource manger的類 但是你定義為枚舉 有點(diǎn)別扭

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

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

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