看公司代碼的時候發(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
方法是線程安全的
-
synchronize
或Lock
很容易想到使用synchronize
或Lock
對方法進(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)用場景】