文章目錄
- 單例模式的應用場景
- 餓漢式單例模式
- 懶漢式單例模式
- 改進:synchronized
- 改進:雙重檢查鎖
- 改進:靜態內部類
- 破壞單例
- 用反射破壞單例
- 用序列化破壞單例
- 解密
- 注冊式單例模式
- 枚舉式單例模式
- 解密
- 容器式單例
- 線程單例實現ThreadLocal
- 單例模式小結
- 參考資料
單例模式的應用場景
單例模式(Singleton Pattern)是指確保一個類在任何情況下都絕對只有一個實例,并提供一個全局訪問點。單例模式是創建型模式。單例模式在現實生活中應用也非常廣泛,例如,公司CEO、部門經理等。J2EE標準中的ServletContext、ServletContextConfig 等、Spring框架應用中的ApplicationContext、數據庫的連接池等也都是單例形式。
單例模式的類結構圖如下:
餓漢式單例模式
餓漢式單例模式在類加載的時候就立即初始化,并且創建單例對象。它絕對線程安全,在線程還沒出現以前就實例化了、不可能存在訪問安全問題。
優點:沒有加任何鎖、執行效率比較高,用戶體驗比懶漢式單例模式更好。
缺點:類加載的時候就初始化,不管用與不用都占著空間,可能浪費內存,“尸位素餐”。
Spring中loC容器ApplicationContext本身就是典型的餓漢式單例模式。
接下來看一段代碼:
public class HungrySingleton {//先靜態、后動態//先屬性、后方法//先上后下private static final HungrySingleton hungrySingleton = new HungrySingleton();private HungrySingleton(){}public static HungrySingleton getInstance(){return hungrySingleton;}
}
還有另外一種寫法,利用靜態代碼塊的機制:
public class HungryStaticSingleton {private static final HungryStaticSingleton hungrySingleton;static {hungrySingleton = new HungryStaticSingleton();}private HungryStaticSingleton(){}public static HungryStaticSingleton getInstance(){return hungrySingleton;}
}
這兩種寫法都非常簡單,也非常好理解,餓漢式單例模式適用于單例對象較少的情況。下面我們來看性能更優的寫法。
ZJ:聯想起掛著大餅的巨嬰。
懶漢式單例模式
懶漢式單例模式的特點是:被外部類調用的時候內部類才會加載。下面看懶漢式單例模式的簡單實現LazySimpleSingleton:
public class LazySimpleSingleton {private LazySimpleSingleton(){}//靜態塊,公共內存區域private static LazySimpleSingleton lazy = null;public static LazySimpleSingleton getInstance(){if(lazy == null){lazy = new LazySimpleSingleton();}return lazy;}public static void main(String[] args) {Runnable task = ()->{LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();System.out.println(Thread.currentThread().getName() + ":" + singleton);};Thread t1 = new Thread(task);Thread t2 = new Thread(task);t1.start();t2.start();System.out.println("End");}}
運行結果如下:
End
Thread-1:com.lun.pattern.singleton.lazy.LazySimpleSingleton@6fc8c462
Thread-0:com.lun.pattern.singleton.lazy.LazySimpleSingleton@6fc8c462
上面的代碼有一定概率出現兩種不同結果,這意味著上面的單例存在線程安全隱患。
我們通過調試運行再具體看一下,手動控制線程的執行順序來跟蹤內存的變化。如下圖打上斷點。
運行調試,讓兩線程停頓在lazy = new LazySimpleSingleton();
先讓Thread-0單步運行,觀察lazy變量的哈希值:
先讓Thread-1單步運行,觀察lazy變量的哈希值:
LazySimpleSingleton類有創建兩次實例,這違背單例模式初衷。
有時我們得到的運行結果可能是相同的兩個對象,實際上是被后面執行的線程覆蓋了,我們看到了一個假象,線程安全隱患依舊存在。
改進:synchronized
那么,我們如何來優化代碼,使得懶漢式單例模式在線程環境下安全呢?來看下面的代碼,給getInstance()加上synchronized關鍵字,使這個方法變成線程同步方法:
public class LazySimpleSingleton {private LazySimpleSingleton(){}//靜態塊,公共內存區域private static LazySimpleSingleton lazy = null;public static synchronized LazySimpleSingleton getInstance(){if(lazy == null){lazy = new LazySimpleSingleton();}return lazy;}
}
運行調試。先讓Thread-0獲得鎖,正在調用getInstance()的lazy = new LazySimpleSingleton();
(斷點保持未填關鍵字synchronized時那樣)。而Thread-1嘗試調用getInstance(),但存在鎖存在,只能被阻塞,直到Thread-0調用getInstance()返回后釋放鎖為止。
上圖完美地展現了synchronized 監視鎖的運行狀態,線程安全的問題解決了。
但是,用synchronized加鎖時,在線程數量比較多的情況下,如果CPU分配壓力上升,則會導致大批線程阻塞,從而導致程序性能大幅下降。
改進:雙重檢查鎖
那么,有沒有一種更好的方式,既能兼顧線程安全又能提高程序性能呢?
答案是肯定的。我們來看雙重檢查鎖的單例模式:
public class LazyDoubleCheckSingleton {private volatile static LazyDoubleCheckSingleton lazy = null;private LazyDoubleCheckSingleton(){}public static LazyDoubleCheckSingleton getInstance(){if(lazy == null){synchronized (LazyDoubleCheckSingleton.class){
// if(lazy == null){lazy = new LazyDoubleCheckSingleton();//1.分配內存給這個對象//2.初始化對象//3.設置lazy指向剛分配的內存地址//4.初次訪問對象
// }}}return lazy;}}
但是,用到 synchronized關鍵字總歸要上鎖,對程序性能還是存在一定影響的。
難道就真的沒有更好的方案嗎?當然有。
改進:靜態內部類
我們可以從類初始化的角度來考慮,看下面的代碼,采用靜態內部類的方式:
public class LazyInnerClassSingleton {//默認使用LazyInnerClassGeneral的時候,會先初始化內部類//如果沒使用的話,內部類是不加載的private LazyInnerClassSingleton(){}//每一個關鍵字都不是多余的//static 是為了使單例的空間共享//保證這個方法不會被重寫,重載public static final LazyInnerClassSingleton getInstance(){//在返回結果以前,一定會先加載內部類return LazyHolder.LAZY;}//默認不加載private static class LazyHolder{private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();}
}
這種方式兼顧了餓漢式單例模式的內存浪費問題和synchronized 的性能問題。內部類一定是要在方法調用之前初始化,巧妙地避免了線程安全問題。
破壞單例
用反射破壞單例
大家有沒有發現,上面介紹的單例模式的構造方法除了加上private關鍵字,沒有做任何處理,如果我們使用反射來調用其構造方法,再調用getInstance()方法,應該有兩個不同的實例。現在來看一段測試代碼,以 LazyInnerClassSingleton為例:
public class LazyInnerClassSingleton {...public static void main(String[] args) {try{//在很無聊的情況下,進行破壞Class<?> clazz = LazyInnerClassSingleton.class;//通過反射獲取私有的構造方法Constructor c = clazz.getDeclaredConstructor(null);//強制訪問c.setAccessible(true);//暴力初始化Object o1 = c.newInstance();//調用了兩次構造方法,相當于“new”了兩次,犯了原則性錯誤Object o2 = c.newInstance();System.out.println(o1 == o2);}catch(Exception e){e.printStackTrace();}}}
輸出結果為:
false
顯然,創建了兩個不同的實例。現在,我們在其構造方法中做一些限制,一旦出現多次重復創建,則直接拋出異常。來看優化后的代碼:
public class LazyInnerClassSingleton {//默認使用LazyInnerClassSingleton的時候,會先初始化內部類//如果沒使用的話,內部類是不加載的private LazyInnerClassSingleton(){if(LazyHolder.LAZY != null){//<------------------------關注點throw new RuntimeException("不允許創建多個實例");}}...}
再次運行測試代碼,輸出結果為:
java.lang.reflect.InvocationTargetExceptionat java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:78)at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)at com.lun.pattern.singleton.lazy.LazyInnerClassSingleton.main(LazyInnerClassSingleton.java:47)
Caused by: java.lang.RuntimeException: 不允許創建多個實例at com.lun.pattern.singleton.lazy.LazyInnerClassSingleton.<init>(LazyInnerClassSingleton.java:20)... 6 more
至此,看起來相當完美單例模式實現了。
用序列化破壞單例
一個單例對象創建好后,有時候需要將對象序列化然后寫入磁盤,下次使用時再從磁盤中讀取對象并進行反序列化,將其轉化為內存對象。反序列化后的對象會重新分配內存,即重新創建。如果序列化的目標對象為單例對象,就違背了單例模式的初衷,相當于破壞了單例,來看一段代碼:
public class SeriableSingleton implements Serializable {//序列化就是說把內存中的狀態通過轉換成字節碼的形式//從而轉換一個IO流,寫入到其他地方(可以是磁盤、網絡IO)//內存中狀態給永久保存下來了//反序列化//講已經持久化的字節碼內容,轉換為IO流//通過IO流的讀取,進而將讀取的內容轉換為Java對象//在轉換過程中會重新創建對象newpublic final static SeriableSingleton INSTANCE = new SeriableSingleton();private SeriableSingleton(){}public static SeriableSingleton getInstance(){return INSTANCE;}
}
測試代碼:
public class SeriableSingletonTest {public static void main(String[] args) {SeriableSingleton s1 = null;SeriableSingleton s2 = SeriableSingleton.getInstance();FileOutputStream fos = null;try {fos = new FileOutputStream("SeriableSingleton.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(s2);oos.flush();oos.close();FileInputStream fis = new FileInputStream("SeriableSingleton.obj");ObjectInputStream ois = new ObjectInputStream(fis);s1 = (SeriableSingleton)ois.readObject();ois.close();System.out.println(s1);System.out.println(s2);System.out.println(s1 == s2);} catch (Exception e) {e.printStackTrace();}}
}
運行結果:
com.lun.pattern.singleton.seriable.SeriableSingleton@17c68925
com.lun.pattern.singleton.seriable.SeriableSingleton@48140564
false
從運行結果可以看出,反序列化后的對象和手動創建的對象是不一致的,實例化了兩次,違背了單例模式的設計初衷。
那么,我們如何保證在序列化的情況下也能夠實現單例模式呢?其實很簡單,只需要增加readResolve()方法即可。來看優化后的代碼:
public class SeriableSingleton implements Serializable {...private Object readResolve(){//新添方法。return INSTANCE;}}
再次運行測試代碼:
com.lun.pattern.singleton.seriable.SeriableSingleton@48140564
com.lun.pattern.singleton.seriable.SeriableSingleton@48140564
true
解密
為什么添加readResolve()后,問題解決了。閱讀ObjectInputStream類的readObject()方法源碼,代碼如下:
public class ObjectInputStreamextends InputStream implements ObjectInput, ObjectStreamConstants
{...public final Object readObject()throws IOException, ClassNotFoundException {return readObject(Object.class);//調用下面那個私有方法}private final Object readObject(Class<?> type)throws IOException, ClassNotFoundException{if (enableOverride) {return readObjectOverride();}if (! (type == Object.class || type == String.class))throw new AssertionError("internal error");// if nested read, passHandle contains handle of enclosing objectint outerHandle = passHandle;try {Object obj = readObject0(type, false);//<-------關注點handles.markDependency(outerHandle, passHandle);ClassNotFoundException ex = handles.lookupException(passHandle);if (ex != null) {throw ex;}if (depth == 0) {vlist.doCallbacks();freeze();}return obj;} finally {passHandle = outerHandle;if (closed && depth == 0) {clear();}}}}
readObject()方法中又調用了重寫的 readObject0()方法。進入readObject0()方法代碼如下:
private Object readObject0(Class<?> type, boolean unshared) throws IOException {...byte tc;while ((tc = bin.peekByte()) == TC_RESET) {bin.readByte();handleReset();}depth++;totalObjectRefs++;try {switch (tc) {...case TC_OBJECT:if (type == String.class) {throw new ClassCastException("Cannot cast an object to java.lang.String");}return checkResolve(readOrdinaryObject(unshared));//<-------關注點...}} finally {depth--;bin.setBlockDataMode(oldMode);}}
MN:這里沒太懂怎么到TC_OBJECT的這步的。
我們看到TC_OBJECT中調用了ObjectInputStream的readOrdinaryObject()方法,看源碼:
public class ObjectInputStreamextends InputStream implements ObjectInput, ObjectStreamConstants
{...private Object readOrdinaryObject(boolean unshared)throws IOException{if (bin.readByte() != TC_OBJECT) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);desc.checkDeserialize();Class<?> cl = desc.forClass();if (cl == String.class || cl == Class.class|| cl == ObjectStreamClass.class) {throw new InvalidClassException("invalid class descriptor");}Object obj;try {obj = desc.isInstantiable() ? desc.newInstance() : null;//<--------------關注點} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}...return obj;}
}
我們發現調用了ObjectStreamClass的isInstantiable()方法,而 isInstantiable()方法的代碼如下:
public class ObjectInputStreamextends InputStream implements ObjectInput, ObjectStreamConstants
{...boolean isInstantiable() {requireInitialized();return (cons != null);}...
}
上述代碼非常簡單,就是判斷一下構造方法是否為空,構造方法不為空就返回 true。這意味著只要有無參構造方法就會實例化。
MN:如果沒添加readResolve()方法,就返回這實例。
此時并沒有找到加上readResolve()方法就避免了單例模式被破壞的真正原因。再回到ObjectInputStream的readOrdinaryObject()方法,繼續往下看:
public class ObjectInputStreamextends InputStream implements ObjectInput, ObjectStreamConstants
{...private Object readOrdinaryObject(boolean unshared)throws IOException{if (bin.readByte() != TC_OBJECT) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);desc.checkDeserialize();Class<?> cl = desc.forClass();if (cl == String.class || cl == Class.class|| cl == ObjectStreamClass.class) {throw new InvalidClassException("invalid class descriptor");}Object obj;try {obj = desc.isInstantiable() ? desc.newInstance() : null;} catch (Exception ex) {throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);}...if (obj != null &&handles.lookupException(passHandle) == null &&desc.hasReadResolveMethod())//<-----關注點{Object rep = desc.invokeReadResolve(obj);if (unshared && rep.getClass().isArray()) {rep = cloneArray(rep);}if (rep != obj) {// Filter the replacement objectif (rep != null) {if (rep.getClass().isArray()) {filterCheck(rep.getClass(), Array.getLength(rep));} else {filterCheck(rep.getClass(), -1);}}handles.setObject(passHandle, obj = rep);}}return obj;}...
}
判斷無參構造方法是否存在之后,又調用了ObjectStreamClass.hasReadResolveMethod()方法,來看代碼:
public class ObjectStreamClass implements Serializable {...boolean hasReadResolveMethod() {requireInitialized();return (readResolveMethod != null);}...
}
上述代碼邏輯非常簡單,就是判斷readResolveMethod是否為空,不為空就返回true。
通過全局查找知道,在私有方法ObjectStreamClass()中給readResolveMethod進行了賦值,來看代碼:
public class ObjectStreamClass implements Serializable {...private ObjectStreamClass(final Class<?> cl) {...readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);... }...}
上面的邏輯其實就是通過反射找到一個無參的readResolve()方法,并且保存下來。現在回到ObjectInputStream 的readOrdinaryObject()方法繼續往下看,如果readResolve()方法存在則調用invokeReadResolve()方法,來看代碼:
public class ObjectStreamClass implements Serializable {...Object invokeReadResolve(Object obj)throws IOException, UnsupportedOperationException{requireInitialized();if (readResolveMethod != null) {try {return readResolveMethod.invoke(obj, (Object[]) null);//<----關注點,調用我們新添的方法。} catch (InvocationTargetException ex) {Throwable th = ex.getTargetException();if (th instanceof ObjectStreamException) {throw (ObjectStreamException) th;} else {throwMiscException(th);throw new InternalError(th); // never reached}} catch (IllegalAccessException ex) {// should not occur, as access checks have been suppressedthrow new InternalError(ex);}} else {throw new UnsupportedOperationException();}}...}
我們可以看到,在invokeReadResolve()方法中用反射調用了readResolveMethod方法。
通過JDK源碼分析我們可以看出,雖然增加readResolve()方法返回實例解決了單例模式被破壞的問題,但是實際上實例化了兩次,只不過新創建的對象沒有被返回而已。
如果創建對象的動作發生頻率加快,就意味著內存分配開銷也會隨之增大。
有辦法從根本上解決問題嗎?下面講的注冊式單例應運而生。
注冊式單例模式
注冊式單例模式又稱為登記式單例模式,就是將每一個實例都登記到某一個地方,使用唯一的標識獲取實例。
注冊式單例模式有兩種:
- 一種為枚舉式單例模式,
- 另一種為容器式單例模式。
枚舉式單例模式
先來看枚舉式單例模式的寫法,創建EnumSingleton類:
public enum EnumSingleton {INSTANCE;private Object data;public Object getData() {return data;}public void setData(Object data) {this.data = data;}public static EnumSingleton getInstance(){return INSTANCE;}
}
測試代碼:
public class EnumSingletonTest {public static void main(String[] args) {try {EnumSingleton instance1 = null;EnumSingleton instance2 = EnumSingleton.getInstance();instance2.setData(new Object());FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(instance2);oos.flush();oos.close();FileInputStream fis = new FileInputStream("EnumSingleton.obj");ObjectInputStream ois = new ObjectInputStream(fis);instance1 = (EnumSingleton) ois.readObject();ois.close();System.out.println(instance1.getData());System.out.println(instance2.getData());System.out.println(instance1.getData() == instance2.getData());}catch (Exception e){e.printStackTrace();}}
}
運行結果:
java.lang.Object@2280cdac
java.lang.Object@2280cdac
true
它竟如此優雅,簡單。
解密
下載一個非常好用的Java反編譯工具 Jad(下載地址: https://varaneckas.com/jad/),解壓后配置好環境變量(或在工具所在目錄下使用),就可以使用命令行調用了。找到工程所在的Class目錄,復制EnumSingleton.class所在的路徑。
然后反編譯EnumSingleton.class
jad D:\eclipse-workspace\lun-spring-2\target\classes\com\lun\pattern\singleton\register\EnumSingleton.class
打開反編譯后生成的EnumSingleton.jad內容如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingleton.javapackage com.lun.pattern.singleton.register;public final class EnumSingleton extends Enum
{private EnumSingleton(String s, int i){super(s, i);}public Object getData(){return data;}public void setData(Object data){this.data = data;}public static EnumSingleton getInstance(){return INSTANCE;}public static EnumSingleton[] values(){EnumSingleton aenumsingleton[];int i;EnumSingleton aenumsingleton1[];System.arraycopy(aenumsingleton = ENUM$VALUES, 0, aenumsingleton1 = new EnumSingleton[i = aenumsingleton.length], 0, i);return aenumsingleton1;}public static EnumSingleton valueOf(String s){return (EnumSingleton)Enum.valueOf(com/lun/pattern/singleton/register/EnumSingleton, s);}public static final EnumSingleton INSTANCE;private Object data;private static final EnumSingleton ENUM$VALUES[];static {//<-----------------------主要關注點INSTANCE = new EnumSingleton("INSTANCE", 0);ENUM$VALUES = (new EnumSingleton[] {INSTANCE});}
}
原來,枚舉式單例模式在靜態代碼塊中就給INSTANCE進行了賦值,是餓漢式單例模式的實現。
至此,我們還可以試想,序列化能否破壞枚舉式單例模式呢?不妨再來看一下JDK源碼,還是回到ObjectInputStream的readObject0()方法:
public class ObjectInputStreamextends InputStream implements ObjectInput, ObjectStreamConstants
{...private Object readObject0(Class<?> type, boolean unshared) throws IOException {...case TC_ENUM:if (type == String.class) {throw new ClassCastException("Cannot cast an enum to java.lang.String");}return checkResolve(readEnum(unshared));...}}
我們看到,在readObject0()中調用了readEnum()方法,來看readEnum()方法的代碼實現:
public class ObjectInputStreamextends InputStream implements ObjectInput, ObjectStreamConstants
{...private Enum<?> readEnum(boolean unshared) throws IOException {if (bin.readByte() != TC_ENUM) {throw new InternalError();}ObjectStreamClass desc = readClassDesc(false);if (!desc.isEnum()) {throw new InvalidClassException("non-enum class: " + desc);}int enumHandle = handles.assign(unshared ? unsharedMarker : null);ClassNotFoundException resolveEx = desc.getResolveException();if (resolveEx != null) {handles.markException(enumHandle, resolveEx);}String name = readString(false);Enum<?> result = null;Class<?> cl = desc.forClass();if (cl != null) {try {@SuppressWarnings("unchecked")Enum<?> en = Enum.valueOf((Class)cl, name);//<-----------------------------------------關注點result = en;} catch (IllegalArgumentException ex) {throw (IOException) new InvalidObjectException("enum constant " + name + " does not exist in " +cl).initCause(ex);}if (!unshared) {handles.setObject(enumHandle, result);}}handles.finish(enumHandle);passHandle = enumHandle;return result;}...}
public abstract class Enum<E extends Enum<E>>implements Constable, Comparable<E>, Serializable {...public static <T extends Enum<T>> T valueOf(Class<T> enumClass,String name) {T result = enumClass.enumConstantDirectory().get(name);if (result != null)return result;if (name == null)throw new NullPointerException("Name is null");throw new IllegalArgumentException("No enum constant " + enumClass.getCanonicalName() + "." + name);}...}
我們發現,枚舉類型其實通過類名(String)和類對象類(Class)找到一個唯一的枚舉對象。因此,枚舉對象不可能被類加載器加載多次。
那么反射是否能破壞枚舉式單例模式呢?來看一段測試代碼:
public class EnumSingletonTest {public static void main(String[] args) {try {Class clazz = EnumSingleton.class;Constructor c = clazz.getDeclaredConstructor();c.newInstance();}catch (Exception e){e.printStackTrace();}}
}
運行結果:
java.lang.NoSuchMethodException: com.lun.pattern.singleton.register.EnumSingleton.<init>()at java.base/java.lang.Class.getConstructor0(Class.java:3517)at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2691)at com.lun.pattern.singleton.test.EnumSingletonTest.main(EnumSingletonTest.java:46)
結果中報的是 java.lang.NoSuchMethodException異常,意思是沒找到無參的構造方法。這時候,我們打開java.lang.Enum的源碼,查看它的構造方法,只有一個protected類型的構造方法:
public abstract class Enum<E extends Enum<E>>implements Constable, Comparable<E>, Serializable {...protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}...
}
再嘗試用其創造實例:
public class EnumSingletonTest {...public static void main(String[] args) {try {Class clazz = EnumSingleton.class;Constructor c = clazz.getDeclaredConstructor(String.class,int.class);c.setAccessible(true);EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("Tom",666);}catch (Exception e){e.printStackTrace();}}}
運行結果:
java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)at com.lun.pattern.singleton.test.EnumSingletonTest.main(EnumSingletonTest.java:60)
這時錯誤已經非常明顯了,“Cannot reflectively create enum objects”,即不能用反射來創建枚舉類型。還是習慣性地想來看看JDK源碼,進入Constructor的newInstance()方法:
public final class Constructor<T> extends Executable {...@CallerSensitive@ForceInline // to ensure Reflection.getCallerClass optimizationpublic T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException{Class<?> caller = override ? null : Reflection.getCallerClass();return newInstanceWithCaller(initargs, !override, caller);}/* package-private */T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller)throws InstantiationException, IllegalAccessException,InvocationTargetException{if (checkAccess)checkAccess(caller, clazz, clazz, modifiers);if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");//<--------------------關注點ConstructorAccessor ca = constructorAccessor; // read volatileif (ca == null) {ca = acquireConstructorAccessor();}@SuppressWarnings("unchecked")T inst = (T) ca.newInstance(args);return inst;}...
}
從上述代碼可以看到,在 newInstance()方法中做了強制性的判斷,如果修飾符是Modifier.ENUM枚舉類型,則直接拋出異常。
枚舉式單例模式也是《Effective Java》書中推薦的一種單例模式實現寫法。JDK枚舉的語法特殊性及反射也為枚舉保駕護航,讓枚舉式單例模式成為一種比較優雅的實現。
容器式單例
public class ContainerSingleton {private ContainerSingleton(){}private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();public static Object getInstance(String className){synchronized (ioc) {if (!ioc.containsKey(className)) {Object obj = null;try {obj = Class.forName(className).newInstance();ioc.put(className, obj);} catch (Exception e) {e.printStackTrace();}return obj;} else {return ioc.get(className);}}}
}
容器式單例模式適用于實例非常多的情況,便于管理。但它是非線程安全的。
MN:非線程安全的???深表疑問,synchronized是干啥???
到此,注冊式單例模式介紹完畢。我們再來看看Spring 中的容器式單例模式的實現代碼:
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
implements AutowirecapableBeanFactory {/*Cache of unfinished FactoryBean instances: FactoryBean name --> Beanwrapper */private final Map<String,Beanwrapper> factoryBeanInstanceCache = new ConcurrentHashNap<>(16)};...
}
線程單例實現ThreadLocal
講講線程單例實現ThreadLocal。ThreadLocal不能保證其創建的對象是全局唯一的,但是能保證在單個線程中是唯一的。下面來看代碼:
public class ThreadLocalSingleton {private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =new ThreadLocal<ThreadLocalSingleton>(){@Overrideprotected ThreadLocalSingleton initialValue() {return new ThreadLocalSingleton();}};private ThreadLocalSingleton(){}public static ThreadLocalSingleton getInstance(){return threadLocalInstance.get();}
}
測試代碼:
public class ThreadLocalSingletonTest {public static void main(String[] args) {System.out.println(ThreadLocalSingleton.getInstance());System.out.println(ThreadLocalSingleton.getInstance());System.out.println(ThreadLocalSingleton.getInstance());System.out.println(ThreadLocalSingleton.getInstance());System.out.println(ThreadLocalSingleton.getInstance());Runnable task = ()->{System.out.println(ThreadLocalSingleton.getInstance());};Thread t1 = new Thread(task);Thread t2 = new Thread(task);t1.start();t2.start();System.out.println("End");}
}
運行結果:
com.lun.pattern.singleton.threadlocal.ThreadLocalSingleton@5e265ba4
com.lun.pattern.singleton.threadlocal.ThreadLocalSingleton@5e265ba4
com.lun.pattern.singleton.threadlocal.ThreadLocalSingleton@5e265ba4
com.lun.pattern.singleton.threadlocal.ThreadLocalSingleton@5e265ba4
com.lun.pattern.singleton.threadlocal.ThreadLocalSingleton@5e265ba4
End
com.lun.pattern.singleton.threadlocal.ThreadLocalSingleton@15864d5a
com.lun.pattern.singleton.threadlocal.ThreadLocalSingleton@481d0703
在主線程中無論調用多少次,獲取到的實例都是同一個,都在兩個子線程中分別獲取到了不同的實例。
那么ThreadLocal是如何實現這樣的效果的呢?單例模式為了達到線程安全的目的,會給方法上鎖,以時間換空間。ThreadLocal將所有的對象全部放在ThreadLocalMap中,為每個線程都提供一個對象,實際上是以空間換時間來實現線程隔離的。
單例模式小結
單例模式可以保證內存里只有一個實例,減少了內存的開銷,還可以避免對資源的多重占用。單例模式看起來非常簡單,實現起來其實也非常簡單,但是在面試中卻是一個高頻面試點。
參考資料
- 《Spring5核心原理與30個類手寫實戰》