單例模式,你真的寫對了嗎蚂且?

看公司代碼的時候發(fā)現(xiàn)項目中單例模式應(yīng)用挺多的配猫,并且發(fā)現(xiàn)的兩處單例模式用的還是不同的方式實現(xiàn)的,那么單例模式到底有幾種寫法呢杏死?單例模式看似很簡單泵肄,但是實際寫起來卻問題多多

本文大綱

  • 什么是單例模式
  • 餓漢式創(chuàng)建單例對象
  • 懶漢式創(chuàng)建單例對象
  • 單例模式的優(yōu)缺點
  • 單例模式的應(yīng)用場景

什么是單例模式

確保某個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例淑翼,并且有兩種創(chuàng)建方式腐巢,一種是餓漢式創(chuàng)建,另外一種是懶漢式創(chuàng)建

餓漢式創(chuàng)建單例模式

餓漢式創(chuàng)建就是在類加載時就已創(chuàng)建好對象窒舟,而不是在需要時在創(chuàng)建對象

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 私有構(gòu)造函數(shù)系忙,不能被外部所訪問
     */
    private HungrySingleton() {}

    /**
     * 返回單例對象
     * */
    public static HungrySingleton getHungrySingleton() {
        return hungrySingleton;
    }
}

說明:

  • 構(gòu)造函數(shù)私有化,保證外部不能調(diào)用構(gòu)造函數(shù)創(chuàng)建對象惠豺,創(chuàng)建對象的行為只能由這個類決定
  • 只能通過getHungrySingleton方法獲取對象
  • HungrySingleton對象已經(jīng)創(chuàng)建完成【在類加載時創(chuàng)建】

缺點:

  • 如果getHungrySingleton一直沒有被使用到银还,有點浪費(fèi)資源

優(yōu)點:

  • ClassLoad保證線程安全

懶漢式創(chuàng)建單例模式

懶漢式創(chuàng)建就是在第一次需要該對象時在創(chuàng)建

  • 存在錯誤的懶漢式創(chuàng)建單例對象
    根據(jù)定義很容易在上面餓漢式的基礎(chǔ)上進(jìn)行修改

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
    
        /**
         * 構(gòu)造函數(shù)私有化
         * */
        private LazySingleton() {
        }
    
        private static LazySingleton getLazySingleton() {
            if (lazySingleton == null) {
                return new LazySingleton();
            }
    
            return lazySingleton;
        }
    }
    

    說明:

    • 構(gòu)造函數(shù)私有化
    • 當(dāng)需要時【getLazySingleton方法調(diào)用時】才創(chuàng)建
      嗯风宁,好像沒什么問題,但是當(dāng)有多個線程同時調(diào)用getLazySingleton方法時蛹疯,此時剛好對象沒有初始化戒财,兩個線程同時通過lazySingleton == null的校驗,將會創(chuàng)建兩個LazySingleton對象捺弦。必須搞點手段使getLazySingleton方法是線程安全的
  • synchronizeLock
    很容易想到使用synchronizeLock對方法進(jìn)行加鎖
    使用synchronize

    public class LazySynchronizeSingleton {
        private static LazySynchronizeSingleton lazySynchronizeSingleton= null;
    
        /**
         * 構(gòu)造函數(shù)私有化
         * */
        private LazySynchronizeSingleton() {
        }
    
        public synchronized static LazySynchronizeSingleton getLazySynchronizeSingleton() {
            if (lazySynchronizeSingleton == null) {
                lazySynchronizeSingleton = new LazySynchronizeSingleton();
            }
    
            return lazySynchronizeSingleton;
        }
    }
    

    使用Lock

    public class LazyLockSingleton {
        private static LazyLockSingleton lazyLockSingleton = null;
    
        /**
        * 鎖
        **/
        private static Lock lock = new ReentrantLock();
    
        /**
         * 構(gòu)造函數(shù)私有化
         * */
        private LazyLockSingleton() {
        }
    
        public static LazyLockSingleton getLazyLockSingleton() {
            try {
                lock.lock();
                if (lazyLockSingleton == null) {
                    lazyLockSingleton = new LazyLockSingleton();
                }
            } finally {
                lock.unlock();
            }
    
            return lazyLockSingleton;
        }
    }
    

    這兩種方式雖然保證了線程安全饮寞,但是性能較差,因為線程不安全主要是由這段代碼引起的:

    if (lazyLockSingleton == null) {
      lazyLockSingleton = new LazyLockSingleton();
    }
    

    給方法加鎖無論對象是否已經(jīng)初始化都會造成線程阻塞列吼。如果對象為null的情況下才進(jìn)行加鎖幽崩,對象不為null的時候則不進(jìn)行加鎖,那么性能將會得到提升寞钥,雙重鎖檢查可以實現(xiàn)這個需求

  • 雙重鎖檢查

在加鎖之前先判斷lazyDoubleCheckSingleton == null是否成立慌申,如果不成立直接返回創(chuàng)建好的對象,成立在加鎖

public class LazyDoubleCheckSingleton {
    /**
     * 使用volatile進(jìn)行修飾理郑,禁止指令重排
     * */
    private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    /**
     * 構(gòu)造函數(shù)私有化
     * */
    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getLazyDoubleCheckSingleton() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }

        return lazyDoubleCheckSingleton;
    }
}

說明:

  • 為什么需要對lazyDoubleCheckSingleton添加volatile修飾符
    因為lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();不是原子性的蹄溉,分為三步:
    • lazyDoubleCheckSingleton分配內(nèi)存
    • 調(diào)用構(gòu)造函數(shù)進(jìn)行初始化
    • lazyDoubleCheckSingleton對象指向分配的內(nèi)存【執(zhí)行完這步lazyDoubleCheckSingleton將不為null】為了提高程序的運(yùn)行效率,編譯器會進(jìn)行一個指令重排您炉,步驟2和步驟三進(jìn)行了重排柒爵,線程1先執(zhí)行了步驟一和步驟三,執(zhí)行完后赚爵,lazyDoubleCheckSingleton不為null棉胀,此時線程2執(zhí)行到if (lazyDoubleCheckSingleton == null),線程2將可能直接返回未正確進(jìn)行初始化的lazyDoubleCheckSingleton對象囱晴。出錯的原因主要是lazyDoubleCheckSingleton未正確初始化完成【寫】膏蚓,但是其他線程已經(jīng)讀取lazyDoubleCheckSingleton的值【讀】,使用volatile可以禁止指令重排序畸写,通過內(nèi)存屏障保證寫操作之前不會調(diào)用讀操作【執(zhí)行if (lazyDoubleCheckSingleton == null)

缺點:

  • 為了保證線程安全,代碼不夠優(yōu)雅過于臃腫

  • 靜態(tài)內(nèi)部類

    public class LazyStaticSingleton {
        /**
         * 靜態(tài)內(nèi)部類
         * */
        private static class LazyStaticSingletonHolder {
            private static LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();
        }
    
        /**
         * 構(gòu)造函數(shù)私有化
         * */
        private LazyStaticSingleton() {
        }
    
        public static LazyStaticSingleton getLazyStaticSingleton() {
            return LazyStaticSingletonHolder.lazyStaticSingleton;
        }
    }
    

    靜態(tài)內(nèi)部類在調(diào)用時才會進(jìn)行初始化氓扛,因此是懶漢式的枯芬,LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();看似是餓漢式的,但是只有調(diào)用getLazyStaticSingleton時才會進(jìn)行初始化采郎,線程安全由ClassLoad保證千所,不用思考怎么加鎖

前面幾種方式實現(xiàn)單例的方式雖然各有優(yōu)缺點,但是基本實現(xiàn)了單例線程安全的要求蒜埋。但是總有人看不慣單例模式勤儉節(jié)約的優(yōu)點淫痰,對它進(jìn)行攻擊。對它進(jìn)行攻擊無非就是創(chuàng)建不只一個類整份,java中創(chuàng)建對象的方式有new待错、clone籽孙、序列化、反射火俄。構(gòu)造函數(shù)私有化不可能通過new創(chuàng)建對象犯建、同時單例類沒有實現(xiàn)Cloneable接口無法通過clone方法創(chuàng)建對象,那剩下的攻擊只有反射攻擊和序列化攻擊了
反射攻擊:

public class ReflectAttackTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //靜態(tài)內(nèi)部類
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        //通過反射創(chuàng)建LazyStaticSingleton
        Constructor<LazyStaticSingleton> constructor = LazyStaticSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance();
        //打印結(jié)果為false瓜客,說明又創(chuàng)建了一個新對象
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

        //synchronize
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        Constructor<LazySynchronizeSingleton> lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor();
        lazySynchronizeSingletonConstructor.setAccessible(true);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance();
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        Constructor<LazyLockSingleton> lazyLockSingletonConstructor = LazyLockSingleton.class.getDeclaredConstructor();
        lazyLockSingletonConstructor.setAccessible(true);
        LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance();
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //雙重鎖檢查
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        Constructor<LazyDoubleCheckSingleton> lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getDeclaredConstructor();
        lazyDoubleCheckSingletonConstructor.setAccessible(true);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance();
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1);
    }
}

都存在反射攻擊适瓦,都可以創(chuàng)建出一個新對象,打印結(jié)果都為false谱仪。針對存在的反射攻擊根據(jù)網(wǎng)上提供的思路在搶救一下玻熙,搶救姿勢如下:

 private LazySynchronizeSingleton() {
      //flag為線程間共享,進(jìn)行加鎖控制
      synchronized (LazySynchronizeSingleton.class) {
          if (flag == false) {
              flag = !flag;
          } else {
              throw new RuntimeException("單例模式被攻擊");
          }
      }
  }

構(gòu)造函數(shù)只能調(diào)用一次疯攒,調(diào)用第二次將拋出異常嗦随,通過flag來判斷構(gòu)造函數(shù)是否已經(jīng)被調(diào)用過一次了。但是我們?nèi)钥梢酝ㄟ^反射修改flag的值:

//調(diào)用反射前將flag設(shè)置為false
Field flagField = lazySynchronizeSingleton.getClass().getDeclaredField("flag");
flagField.setAccessible(true);
flagField.set(lazySynchronizeSingleton, false);

搶救失敗卸例,你可能想通過final修飾禁止修改称杨,但是反射可以先去除final,在加上final修改值筷转,對于反射攻擊姑原,無力回天,只能選擇不適用存在反射攻擊的單例創(chuàng)建方式

反序列化攻擊:

public class SerializableAttackTest {
    public static void main(String[] args) {
        //懶漢式
        HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
        //序列化
        byte[] serialize = SerializationUtils.serialize(hungrySingleton);
        //反序列化
        HungrySingleton hungrySingleton1 = SerializationUtils.deserialize(serialize);
        System.out.println(hungrySingleton == hungrySingleton1);

        //雙重鎖
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
        byte[] serialize1 = SerializationUtils.serialize(lazyDoubleCheckSingleton);
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton11 = SerializationUtils.deserialize(serialize1);
        System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton11);

        //lock
        LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
        byte[] serialize2 = SerializationUtils.serialize(lazyLockSingleton);
        LazyLockSingleton lazyLockSingleton1 = SerializationUtils.deserialize(serialize2);
        System.out.println(lazyLockSingleton == lazyLockSingleton1);

        //synchronie
        LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
        byte[] serialize3 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazySynchronizeSingleton lazySynchronizeSingleton1 = SerializationUtils.deserialize(serialize3);
        System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);

        //靜態(tài)內(nèi)部類
        LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
        byte[] serialize4 = SerializationUtils.serialize(lazySynchronizeSingleton);
        LazyStaticSingleton lazyStaticSingleton1 = SerializationUtils.deserialize(serialize4);
        System.out.println(lazyStaticSingleton == lazyStaticSingleton1);

    }
}

打印結(jié)果都為false呜舒,都存在反序列化攻擊
對于反序列化攻擊锭汛,還是有有效的搶救方式的,搶救姿勢如下:

private Object readResolve() {
    return lazySynchronizeSingleton;
}
復(fù)制代碼

添加readResolve方法并返回創(chuàng)建的單例對象袭蝗,至于搶救的原理唤殴,可以通過跟進(jìn)SerializationUtils.deserialize的代碼可知
上述實現(xiàn)單例對象的方式既要考慮線程安全、又要考慮攻擊到腥,而通過枚舉創(chuàng)建單例對象完全不用擔(dān)心這些問題

  • 枚舉

    public enum EnumSingleton {
        INSTANCE;
    
        public static EnumSingleton getEnumSingleton() {
            return INSTANCE;
        }
    }
    

    代碼實現(xiàn)也相當(dāng)優(yōu)美朵逝,總共才8行代
    實現(xiàn)原理:枚舉類的域(field)其實是相應(yīng)的enum類型的一個實例對象
    可以參考:implementing-singleton-with-an-enum-in-java
    枚舉攻擊測試:

    public class EnumAttackTest {
      public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
          EnumSingleton enumSingleton = EnumSingleton.getEnumSingleton();
          //序列化攻擊
          byte[] serialize4 = SerializationUtils.serialize(enumSingleton);
          EnumSingleton enumSingleton2 = SerializationUtils.deserialize(serialize4);
          System.out.println(enumSingleton == enumSingleton2);
    
          //反射攻擊
          Constructor<EnumSingleton> enumSingletonConstructor = EnumSingleton.class.getDeclaredConstructor();
          enumSingletonConstructor.setAccessible(true);
          EnumSingleton enumSingleton1 = enumSingletonConstructor.newInstance();
          System.out.println(enumSingleton == enumSingleton1);
      }
    }
    

    反射攻擊將會拋出異常,序列化攻擊對它無效乡范,打印結(jié)果為true配名,用枚舉創(chuàng)建單例對象真的是無懈可擊

單例模式的優(yōu)點

  • 只創(chuàng)建了一個實例,節(jié)省內(nèi)存開銷
  • 減少了系統(tǒng)的性能開銷晋辆,創(chuàng)建對象回收對象對性能都有一定的影響
  • 避免對資源的多重占用
  • 在系統(tǒng)設(shè)置全局的訪問點渠脉,優(yōu)化和共享資源優(yōu)化

總結(jié)一下就是節(jié)約資源、提升性能

單例模式的缺點

  • 不適用于變化的對象
  • 單例模式中沒有抽象層瓶佳,擴(kuò)展有困難
  • 與單一原則沖突芋膘。一個類應(yīng)該只實現(xiàn)一個邏輯,而不關(guān)心它是否單例,是不是單例應(yīng)該由業(yè)務(wù)決定

單例模式的應(yīng)用場景

  • Spring IOC默認(rèn)使用單例模式創(chuàng)建bean
  • 創(chuàng)建對象需要消耗的資源過多時
  • 需要定義大量的靜態(tài)常量和靜態(tài)方法的環(huán)境为朋,比如工具類【感覺是最常見應(yīng)用場景】

小結(jié)

總共介紹了六種正確創(chuàng)建單例對象的方式臂拓,推薦使用餓漢式創(chuàng)建單例對象的方式,如果對資源使用有要求潜腻,則推薦使用靜態(tài)內(nèi)部類【注意反序列化攻擊】埃儿,其他方式在保證線程安全的同時對性能將會有影響。枚舉類其實是非常不錯的融涣,線程安全童番、不存在反射攻擊和反序列化攻擊,但是感覺這種創(chuàng)建單例方式應(yīng)用較少威鹿,公司代碼中使用的是雙重鎖檢查和靜態(tài)內(nèi)部類【存在反序列化攻擊】創(chuàng)建單例方式剃斧,甚至之前出去面試時面試官讓寫一個單例,我使用的是枚舉方式忽你,面試官都不知道有這種方式

附:完整例子代碼+測試代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末幼东,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子科雳,更是在濱河造成了極大的恐慌根蟹,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件糟秘,死亡現(xiàn)場離奇詭異简逮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)尿赚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門散庶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人凌净,你說我怎么就攤上這事悲龟。” “怎么了冰寻?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵须教,是天一觀的道長。 經(jīng)常有香客問我斩芭,道長没卸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任秒旋,我火速辦了婚禮,結(jié)果婚禮上诀拭,老公的妹妹穿的比我還像新娘迁筛。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布细卧。 她就那樣靜靜地躺著尉桩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贪庙。 梳的紋絲不亂的頭發(fā)上蜘犁,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機(jī)與錄音止邮,去河邊找鬼这橙。 笑死,一個胖子當(dāng)著我的面吹牛导披,可吹牛的內(nèi)容都是我干的屈扎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼撩匕,長吁一口氣:“原來是場噩夢啊……” “哼鹰晨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起止毕,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤模蜡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扁凛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忍疾,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年令漂,在試婚紗的時候發(fā)現(xiàn)自己被綠了膝昆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡叠必,死狀恐怖荚孵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纬朝,我是刑警寧澤收叶,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站共苛,受9級特大地震影響判没,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜隅茎,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一澄峰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辟犀,春花似錦俏竞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玻佩。三九已至,卻和暖如春席楚,著一層夾襖步出監(jiān)牢的瞬間咬崔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工烦秩, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留垮斯,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓闻镶,卻偏偏與公主長得像甚脉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子铆农,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容