前言
歡迎最美最帥的你點贊哦~~~!
單例模式是面向?qū)ο蟮木幊陶Z言23種設(shè)計模式之一,屬于創(chuàng)建型設(shè)計模式。主要用于解決對象的頻繁創(chuàng)建與銷毀問題,因為單例模式保證一個類僅會有一個實例。大部分對單例模式應(yīng)該都知道一些,但面試的時候可能回答不會很完整,不能給自己加分,甚至扣分。
單一的知識點并不能對自己在面試的時候帶來加分,而系統(tǒng)的只是則會讓面試官另眼相看,而本文會系統(tǒng)的介紹單例模式的基礎(chǔ)版本與完美版本,基本上將單例模式的內(nèi)容完全包括。如果認為有不同的意見可以留言交流。
源碼已收錄github 查看源碼
單例模式最重要的就是保證一個類只會出現(xiàn)一個實例,那么超過一個就不能被稱為是單例,所有其代碼構(gòu)成如下特點。
私有化構(gòu)造器,禁止從外部創(chuàng)建單例對象。
提供一個全局的訪問點獲取單例對象。
什么是全局訪問點? 好吧,上面的話語太文鄒鄒了,如果我說公共的靜態(tài)方法呢?
餓漢、懶漢
主要分為餓漢模式和懶漢模式。那何為餓漢?何為懶漢?
小麗的爸爸從小生活很艱苦,經(jīng)歷了饑荒年代,所以對食物非常緊張。當(dāng)小麗去上學(xué)的時候,不管小麗是否需要,都會給小麗準備很多的零食。
而小明的爸爸則是一個非常懶惰的人,所有的事情都會到最后才去做,所有事情只有當(dāng)有別人來叫他的時候,他才會把事情做完
這樣就引出了我們對餓漢模式和懶漢模式的定義:
餓漢模式:不管單例對象是否被使用,都會先創(chuàng)建出一個對象。餓漢模式存在資源浪費的問題,因為很有可能對象創(chuàng)建出來只會永遠都不會被使用到。
代碼如下:
package demo.single;
/**
* 餓漢模式
*/
public class HungrySingle {
/**
* 餓漢模式,不管hungrySingle對象是否有使用到,都會先創(chuàng)建出來
* 由于餓漢模式在對象使用之前就已經(jīng)被創(chuàng)建,所以是不會存在線程安全問題
*/
private static HungrySingle hungrySingle = new HungrySingle();
/**
* 私有化構(gòu)造器,禁止外部創(chuàng)建
*/
private HungrySingle(){
}
/**
* 提供獲取實例的方法
*/
public static HungrySingle getInstance(){
return hungrySingle;
}
}
懶漢模式:不會先將對象創(chuàng)建出來,而是等到有人使用的時候才會創(chuàng)建。相比餓漢模式,懶漢模式不會存在資源浪費的情況,所以基本都會選擇懶漢模式。
代碼如下:
package demo.single;
/**
* 懶漢模式
*/
public class LazySingle {
/**
* 懶漢模式,不會先創(chuàng)建對象,而是在調(diào)用的時候才會創(chuàng)建對象
*/
private static LazySingle lazySingle = null;
private LazySingle() {
}
/**
* 調(diào)用的時候創(chuàng)建對象并返回
*/
public static LazySingle getInstance(){
if(lazySingle == null){
lazySingle = new LazySingle();
}
return lazySingle;
}
}
小李:面試官,您看我這樣的解釋可還行。
面試官:單線程下是挺好的,如果在多線程環(huán)境下呢?
小李:這個我知道,加鎖?。?/p>
面試官:出門左轉(zhuǎn)電梯直達!
其實加鎖也沒答錯,關(guān)鍵問題在于如何加鎖!
直接將獲取實例的方法內(nèi)容寫入同步代碼塊中,解決了多線程安全的問題,但是并發(fā)效率的問題又暴露了出來。你想啊,現(xiàn)在鎖住了這方法,而無論單例的對象是否創(chuàng)建,都會經(jīng)過獲取鎖、釋放鎖的過程。這樣的性能顯然是不能接受的。
小李:我想想啊~~~! Emmmmm...! 有了,我們可以在同步代碼塊外層加一個判斷,如果對象已經(jīng)創(chuàng)建則直接返回。
面試官:這樣解決了一部分的并發(fā)效率問題,但是如果在創(chuàng)建的時候同時有很多的線程訪問,是不是也會有并發(fā)的效率問題呢?再優(yōu)化優(yōu)化。
小李一想,確實是這樣,如果對象還沒有創(chuàng)建出來的時候,就有很多的線程來訪問,也會出現(xiàn)問題,假設(shè)有兩個線程同時訪問,當(dāng)A線程優(yōu)先爭搶到鎖,A進入同步代碼塊執(zhí)行,此時B沒有爭搶到鎖,將處于等待狀態(tài),而當(dāng)A線程執(zhí)行完成后釋放鎖,B進入同步代碼塊執(zhí)行,此時B線程同樣會創(chuàng)建出一個對象,破壞了單例。
小李:面試官,我明白了,可以在同步代碼塊中再加一層if判斷,如果對象已經(jīng)創(chuàng)建,就直接返回即可。
Double Check
上面最后的結(jié)果就是我們常說的Double Check,即雙重鎖檢查。雙重鎖檢查在很多地方都被運用到,代碼如下。
package demo.single;
/**
* 懶漢模式
*/
public class LazySingle {
/**
* 懶漢模式,不會先創(chuàng)建對象,而是在調(diào)用的時候才會創(chuàng)建對象
*/
private static LazySingle lazySingle = null;
private LazySingle() {
}
/**
* 調(diào)用的時候創(chuàng)建對象并返回
*/
public static LazySingle getInstance(){
//first check
if(lazySingle != null){
synchronized (LazySingle.class){
//double check
if(lazySingle == null){
lazySingle = new LazySingle();
}
}
}
return lazySingle;
}
}
面試官:小李,你多線程運行一下代碼看看呢。
小李:好勒! 好像挺正常啊。等等, 好像不對, 這里還是出現(xiàn)了多個對象!!!啊~~,這是為什么啊,我都懵了,這完全超出了我的能力范圍。
面試官:哈哈,小子,這下知道誰是大佬了吧?我來給你好好解釋一下,其實,這和我們的代碼沒有關(guān)系,正常來講,應(yīng)該不會出現(xiàn)這樣的問題,但是我們都知道,代碼在運行過程中,會被編譯成一條一條的指令運行,而JVM在運行時,在保證單線程最終結(jié)果不會受影響的情況下,對指令進行優(yōu)化,就有可能對指令進行重排序,同樣會破壞單例。
lazySingle = new LazySingle();
//這樣一段代碼在運行時會生成3條指令,即: 1. 分配內(nèi)存空間 2. 創(chuàng)建對象 3. 指向引用
//正常情況下是會按照1 2 3順序執(zhí)行,但JVM優(yōu)化器進行指令重排后,則可能變?yōu)椋?. 分配內(nèi)存空間 3. 指向引用 2. 創(chuàng)建對象
//在單線程下,這樣的優(yōu)化沒有問題,但是多線程下,線程是在爭搶CPU時間碎片的。假設(shè)A剛剛執(zhí)行完 1 3 //條指令,此時B爭搶到時間碎片,發(fā)現(xiàn)對象不為空了,就直接返回,但此時對象還沒有真正被創(chuàng)建。B調(diào)用
//此對象就會拋出異常
//而volatile關(guān)鍵字修飾的變量可以禁止指令重排序,則可以保證指令會是1 2 3順序執(zhí)行。
//加上volatile修飾
private volatile static LazySingle lazySingle = null;
小李: 終于解決了,好難啊,一個簡單的單例模式居然有這么多的細節(jié)。
面試官:你以為這就完了?
內(nèi)部類的單例
使用內(nèi)部類的方式可以非常完美的完成單例模式,而實現(xiàn)代碼也非常簡單。
package demo.single;
/**
* 內(nèi)部類的方式實現(xiàn)單例
*/
public class InnerSingle {
/**
* 私有化構(gòu)造器
*/
private InnerSingle(){
}
/**
* 私有內(nèi)部類
*/
private static class Inner{
//Jingtai內(nèi)部類持有外部類的對象
public static final InnerSingle SINGLE = new InnerSingle();
}
/**
* 返回靜態(tài)內(nèi)部類持有的對象
*/
public static InnerSingle getInstance(){
return Inner.SINGLE;
}
}
可以看到,代碼中并沒有出現(xiàn)同步方法或者同步代碼塊,那么靜態(tài)內(nèi)部類的方式是如何做到安全的單例模式呢?
外部類加載的時候,不會立即加載內(nèi)部類,而是在調(diào)用的時候會加載內(nèi)部類。
不管多少線程訪問,JVM一定會保證類被正確的初始化,即靜態(tài)內(nèi)部類的方式是在JVM層面保證了線程安全
當(dāng)然,這樣也有一些缺點,那就是在創(chuàng)建單例對象的時候,如果需要傳參,那么靜態(tài)內(nèi)部類的方式會非常麻煩。
破壞單例
那么,上面的單例已經(jīng)完美了嗎?并沒有,看我如何將單例給破壞掉。
反射破壞
反射可以繞過私有構(gòu)造器的限制,創(chuàng)建對象。當(dāng)然正常的調(diào)用是不會發(fā)生單例被破壞的情況,但是如果偏偏有人不走尋常路呢,比如下面的調(diào)用。
package demo.single;
import java.lang.reflect.Constructor;
/**
* 反射破壞單例
*/
public class RefBreakSingleTest {
public static void main(String[] args) throws Exception {
//獲取類對象
Class<LazySingle> lazySingleClass = LazySingle.class;
//獲取構(gòu)造器
Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
//創(chuàng)建對象
LazySingle lazySingle = constructor.newInstance(null);
System.out.println(lazySingle);
System.out.println(LazySingle.getInstance());
System.out.println(lazySingle == LazySingle.getInstance());
}
}
很明顯看到出現(xiàn)了兩個不同的兌現(xiàn),顯然,單例被破壞了!
對于這樣的情況該如何禁止呢?在網(wǎng)上查閱了很多資料,大部分是使用變量控制法,即在類中添加一個變量用于判斷單例類的構(gòu)造器是否有被調(diào)用,代碼如下。
//添加變量控制,防止反射破壞
private static boolean isInstance = false;
private volatile static LazySingle lazySingle = null;
private LazySingle() throws Exception {
if(isInstance){
throw new Exception("the Constructor has be used");
}
isInstance = true;
}
再次調(diào)用測試代碼,發(fā)現(xiàn)不能再創(chuàng)建多個單例對象,程序拋出了異常。
但是別忘了,屬性也是可以通過反射修改的(count、instance的判斷反射都能繞過)。
public class RefBreakSingleTest {
public static void main(String[] args) throws Exception {
//獲取類對象
Class<LazySingle> lazySingleClass = LazySingle.class;
//獲取構(gòu)造器
Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
//創(chuàng)建對象
LazySingle lazySingle = constructor.newInstance(null);
System.out.println(lazySingle);
Field isInstance = lazySingleClass.getDeclaredField("isInstance");
isInstance.setAccessible(true);
isInstance.set(null,false);
System.out.println(LazySingle.getInstance());
System.out.println(lazySingle == LazySingle.getInstance());
}
}
單例再次被破壞,感覺是不是已經(jīng)快崩潰了,一個單例咋這么多事呢??!既然私有屬性、私有方法在外部都能通過反射獲取,那有沒有反射不能獲取的呢?我在網(wǎng)上也找到了另外一種寫法,即私有內(nèi)部類的來持有實例控制變量,而我也通過測試,發(fā)現(xiàn)反射同樣能夠繞過從而破壞單例。
package demo.pattren.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class BreakInnerTest {
public static void main(String[] args) throws Exception {
Class<LazySingle> lazySingleClass = LazySingle.class;
// //獲取構(gòu)造器
Constructor<LazySingle> constructor = lazySingleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
//創(chuàng)建對象
LazySingle lazySingle = constructor.newInstance(null);
//獲取內(nèi)部類的類對象
Class<?> aClass = Class.forName("demo.pattren.single.LazySingle$InnerClass");
Method[] methods = aClass.getMethods();
Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
System.out.println(declaredConstructors);
Constructor<?> declaredConstructor = declaredConstructors[0];
declaredConstructor.setAccessible(true);
//創(chuàng)建內(nèi)部類需要傳入一個外部類的對象
Object o = declaredConstructor.newInstance(lazySingle);
//成功繞過
methods[0].invoke(o);
}
}
目前網(wǎng)上基本都是這兩種,但是反射都是能夠繞過判斷進行破壞。可以這樣認為,這種方式反射是可以破壞的,不能100%保證單例不被破壞。歡迎各位提供完美的示例。
序列化破壞
Java的IO提供了對象流,用來將對象寫入磁盤、從磁盤讀取對象的功能。這也成為了單例的破壞點。
public static void main(String[] args) throws Exception {
//正常的方式獲取單例對象
InnerSingle instance = InnerSingle.getInstance();
//寫入磁盤
FileOutputStream fos = new FileOutputStream("d:/single");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.close();
fos.close();
//從磁盤讀取對象
FileInputStream fis = new FileInputStream("d:/single");
ObjectInputStream ois = new ObjectInputStream(fis);
InnerSingle innerSingle = (InnerSingle) ois.readObject();
System.out.println(instance);
System.out.println(innerSingle);
System.out.println(innerSingle == instance);
}
而序列化的方式JVM提供了一種機制,可以防止單例被破壞,即在單例類中添加readResovle方法。
//在反序列化時,readResolve方法,則直接返回該方法指定的對象
private Object readResolve(){
return getInstance();
}
測試結(jié)果:
序列化沒有再破壞單例,而這一切JDK是如何處理的呢?
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
int outerHandle = passHandle;
try {
//關(guān)鍵代碼,最終返回的是此方法返回的對象
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
//more code but not importent
繼續(xù)深入,發(fā)現(xiàn)readObject0方法的關(guān)鍵代碼如下
byte tc;
//取出文件的一個字節(jié),判斷讀取的對象類型
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_ENUM:
return checkResolve(readEnum(unshared));
//判斷為對象類
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
//more othrer case
繼續(xù)追蹤readOrdinaryObject方法,發(fā)現(xiàn)readReslove的關(guān)鍵代碼
//判斷是否有readReslove方法(desc.hasReadResolveMethod())
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
//執(zhí)行readReslove
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
//最終返回readReslove方法的執(zhí)行結(jié)果
handles.setObject(passHandle, obj = rep);
}
}
return obj;
枚舉單例 - 最完美的單例模式
大神Josh Bloch在《Effective Java》中極力推薦使用枚舉的方式來實現(xiàn)單例。
package demo.single;
public enum EnumSingle {
SINGLE;
public void doJob(){
System.out.println("doJob");
}
}
枚舉類型是單例模式的最佳選擇,主要得益于JVM對于枚舉類型的支持:
JVM保證枚舉類型的每個實例僅存在一份
枚舉類型的序列化與反序列化不會破壞其單例的特性(上面的源碼大家可以去找一找)
反射也不能破壞枚舉單例
可以說,枚舉天然就是單例的,那么你會選擇枚舉作為單例嗎?