一、掌握單例模式的應用場景。java
二、掌握IDEA環境下的多線程調試方式。git
三、掌握保證線程安全的單例模式策略。面試
四、掌握反射暴力攻擊單例解決方案及原理分析。spring
五、序列化破壞單例的原理及解決方案。數據庫
六、掌握常見的單例模式寫法。緩存
一、據說過單例模式,但不知道如何應用的人羣。安全
二、單例模式是很是經典的高頻面試題,但願經過面試單例彰顯技術深度,順利拿到Offer的人羣。多線程
單例模式(SingletonPattern)是指確保一個類在任何狀況下都絕對只有一個實例,並提供一個全局訪問點。單例模式是建立型模式。單例模式在現實生活中應用也很是普遍,例如,公司CEO、部門經
理 等 。 J2EE 標 準 中 的 ServletContext 、 ServletContextConfig 等 、 Spring 框 架 應 用 中 的
ApplicationContext、數據庫的鏈接池BDPool等也都是單例形式。app
/** * 優勢:執行效率高,性能高,沒有任何的鎖 * 缺點:某些狀況下,可能會形成內存浪費 */ 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; //裝個B static { hungrySingleton = new HungryStaticSingleton(); } private HungryStaticSingleton(){} public static HungryStaticSingleton getInstance(){ return hungrySingleton; } }
<img src="https://gitee.com/woshiamiaojiang/image-hosting/raw/master/HungrySingleton.png" style="zoom:50%;" />ide
優勢:沒有加任何鎖、執行效率比較高,用戶體驗比懶漢式單例模式更好。
缺點:類加載的時候就初始化,無論用與不用都佔着空間,浪費了內存,有可能「佔着茅坑不拉屎」。
Spring中IoC容器ApplicationContext自己就是典型的餓漢式單例模式
懶漢式單例模式的特色是:被外部類調用的時候內部類纔會加載。
/** * 優勢:節省了內存,線程安全 * 缺點:性能低 */ //懶漢式單例模式在外部須要使用的時候才進行實例化 public class LazySimpleSingletion { private static LazySimpleSingletion instance; //靜態塊,公共內存區域 private LazySimpleSingletion(){} public synchronized static LazySimpleSingletion getInstance(){ if(instance == null){ instance = new LazySimpleSingletion(); } return instance; } } public class ExectorThread implements Runnable { public void run() { LazySimpleSingletion instance = LazySimpleSingletion.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + instance); } } public class LazySimpleSingletonTest { public static void main(String[] args) { Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("End"); } }
給getInstance()加上synchronized關鍵字,使這個方法變成線程同步方法:
當執行其中一個線程並調用getInstance()方法時,另外一個線程在調用getInstance()
方法,線程的狀態由 RUNNING 變成了 MONITOR,出現阻塞。直到第一個線程執行完,第二個線程
才恢復到RUNNING狀態繼續調用getInstance()方法
<img src="https://gitee.com/woshiamiaojiang/image-hosting/raw/master/image-20200227132959169.png" alt="image-20200227132959169" style="zoom:50%;" />
上圖完美地展示了 synchronized 監視鎖的運行狀態,線程安全的問題解決了。可是,用
synchronized加鎖時,在線程數量比較多的狀況下,若是CPU分配壓力上升,則會致使大批線程阻塞,
從而致使程序性能大幅降低。那麼,有沒有一種更好的方式,既能兼顧線程安全又能提高程序性能呢?
答案是確定的。咱們來看雙重檢查鎖的單例模式:
/** * 優勢:性能高了,線程安全了 * 缺點:可讀性難度加大,不夠優雅 */ public class LazyDoubleCheckSingleton { // volatile解決指令重排序 private volatile static LazyDoubleCheckSingleton instance; private LazyDoubleCheckSingleton() { } public static LazyDoubleCheckSingleton getInstance() { //檢查是否要阻塞,第一個instance == null是爲了建立後再也不走synchronized代碼,提升效率。能夠理解是個開關。建立後這個開關就關上,後面的代碼就不用執行了。 if (instance == null) { synchronized (LazyDoubleCheckSingleton.class) { //檢查是否要從新建立實例 if (instance == null) { instance = new LazyDoubleCheckSingleton(); //指令重排序的問題 //1.分配內存給這個對象 //2.初始化對象 //3.設置 lazy 指向剛分配的內存地址 } } } return instance; } } public class ExectorThread implements Runnable { public void run() { LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + instance); } } public class LazySimpleSingletonTest { public static void main(String[] args) { Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("End"); } }
當第一個線程調用 getInstance()方法時,第二個線程也能夠調用。當第一個線程執行到
synchronized時會上鎖,第二個線程就會變成 MONITOR狀態,出現阻塞。此時,阻塞並非基於整
個LazySimpleSingleton類的阻塞,而是在getInstance()方法內部的阻塞,只要邏輯不太複雜,對於
調用者而言感知不到。
可是,用到 synchronized 關鍵字總歸要上鎖,對程序性能仍是存在必定影響的。難道就真的沒有更好的方案嗎?固然有。咱們能夠從類初始化的角度來考慮,看下面的代碼,採用靜態內部類的方式:
/* ClassPath : LazyStaticInnerClassSingleton.class LazyStaticInnerClassSingleton$LazyHolder.class 優勢:寫法優雅,利用了Java自己語法特色,性能高,避免了內存浪費,不能被反射破壞 缺點:不優雅 */ //這種形式兼顧餓漢式單例模式的內存浪費問題和 synchronized 的性能問題 //完美地屏蔽了這兩個缺點 //自認爲史上最牛的單例模式的實現方式 public class LazyStaticInnerClassSingleton { //使用 LazyInnerClassGeneral 的時候,默認會先初始化內部類 //若是沒使用,則內部類是不加載的 private LazyStaticInnerClassSingleton(){ // if(LazyHolder.INSTANCE != null){ // throw new RuntimeException("不容許非法建立多個實例"); // } } //每個關鍵字都不是多餘的,static 是爲了使單例的空間共享,保證這個方法不會被重寫、重載 private static LazyStaticInnerClassSingleton getInstance(){ //在返回結果之前,必定會先加載內部類 return LazyHolder.INSTANCE; } //默認不加載 private static class LazyHolder{ private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton(); } }
這種方式兼顧了餓漢式單例模式的內存浪費問題和 synchronized 的性能問題。內部類必定是要在方法調用以前初始化,巧妙地避免了線程安全問題。因爲這種方式比較簡單,咱們就不帶你們一步一步
調試了。
內部類語法特性 : 內部類用時才加載
public class ReflectTest { public static void main(String[] args) { try { //在很無聊的狀況下,進行破壞 Class<?> clazz = LazyStaticInnerClassSingleton.class; //經過反射獲取私有的構造方法 Constructor c = clazz.getDeclaredConstructor(null); //強制訪問 c.setAccessible(true); //暴力初始化 Object instance1 = c.newInstance(); //調用了兩次構造方法,至關於「new」了兩次,犯了原則性錯誤 Object instance2 = c.newInstance(); System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); // Enum }catch (Exception e){ e.printStackTrace(); } } } com.gupaoedu.vip.pattern.singleton.lazy.LazyStaticInnerClassSingleton@64cee07 com.gupaoedu.vip.pattern.singleton.lazy.LazyStaticInnerClassSingleton@1761e840 false
你們有沒有發現,上面介紹的單例模式的構造方法除了加上 private 關鍵字,沒有作任何處理。如
果咱們使用反射來調用其構造方法,再調用 getInstance()方法,應該有兩個不一樣的實例。如今來看一
段測試代碼,以LazyInnerClassSingleton爲例:
顯然,建立了兩個不一樣的實例。如今,咱們在其構造方法中作一些限制,一旦出現屢次重複建立,
則直接拋出異常。因此須要在私有構造方法添加異常:
private LazyStaticInnerClassSingleton(){ if(LazyHolder.INSTANCE != null){ throw new RuntimeException("不容許非法建立多個實例"); } }
一個單例對象建立好後,有時候須要將對象序列化而後寫入磁盤,下次使用時再從磁盤中讀取對象
並進行反序列化,將其轉化爲內存對象。反序列化後的對象會從新分配內存,即從新建立。若是序列化
的目標對象爲單例對象,就違背了單例模式的初衷,至關於破壞了單例,來看一段代碼:
//反序列化致使破壞單例模式 public class SeriableSingleton implements Serializable { //序列化 //把內存中對象的狀態轉換爲字節碼的形式 //把字節碼經過IO輸出流,寫到磁盤上 //永久保存下來,持久化 //反序列化 //將持久化的字節碼內容,經過IO輸入流讀到內存中來 //轉化成一個Java對象 // 餓漢式 public final static SeriableSingleton INSTANCE = new SeriableSingleton(); private SeriableSingleton(){} public static SeriableSingleton getInstance(){ return INSTANCE; } // private Object readResolve(){ 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.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@68837a77 com.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@4b6995df false
從運行結果能夠看出,反序列化後的對象和手動建立的對象是不一致的,實例化了兩次,違背了單
例模式的設計初衷。那麼,咱們如何保證在序列化的狀況下也可以實現單例模式呢?其實很簡單,只需
要增長readResolve()方法便可。
再看運行結果,以下圖所示。
com.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@4b6995df com.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@4b6995df true
你們必定會想:這是什麼緣由呢?爲何要這樣寫?看上去很神奇的樣子,也讓人有些費解。不如
咱們一塊兒來看看JDK的源碼實現以瞭解清楚。咱們進入ObjectInputStream類的readObject()方法,
代碼以下:
public final Object readObject() throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(false); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } }
咱們發現,在readObject()方法中又調用了重寫的readObject0()方法。進入readObject0()方法,
代碼以下:
private Object readObject0(boolean unshared) throws IOException { ... case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); ... }
咱們看到TC_OBJECT中調用了ObjectInputStream的readOrdinaryObject()方法,看源碼:
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()方法的代碼以下:
boolean isInstantiable() { requireInitialized(); return (cons != null); }
上述代碼很是簡單,就是判斷一下構造方法是否爲空,構造方法不爲空就返回true。這意味着只要
有無參構造方法就會實例化。
這時候其實尚未找到加上 readResolve()方法就避免了單例模式被破壞的真正緣由。再回到
ObjectInputStream的readOrdinaryObject()方法,繼續往下看:
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 object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } } return obj; }
判斷無參構造方法是否存在以後,又調用了hasReadResolveMethod()方法,來看代碼:
boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }
上述代碼邏輯很是簡單,就是判斷 readResolveMethod 是否爲空,不爲空就返回 true。那麼
readResolveMethod是在哪裏賦值的呢?經過全局查找知道,在私有方法 ObjectStreamClass()中給
readResolveMethod進行了賦值,來看代碼:
private final void requireInitialized() { if (!initialized) throw new InternalError("Unexpected call when not initialized"); }
上面的邏輯其實就是經過反射找到一個無參的 readResolve()方法,而且保存下來。如今回到
ObjectInputStream 的 readOrdinaryObject()方法繼續往下看,若是 readResolve()方法存在則調用
invokeReadResolve()方法,來看代碼:
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 suppressed throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } }
咱們能夠看到,在invokeReadResolve()方法中用反射調用了readResolveMethod方法。
經過JDK源碼分析咱們能夠看出,雖然增長 readResolve()方法返回實例解決了單例模式被破壞的
問題,可是實際上實例化了兩次,只不過新建立的對象沒有被返回而已。若是建立對象的動做發生頻率加快,就意味着內存分配開銷也會隨之增大,難道真的就沒辦法從根本上解決問題嗎?下面講的註冊式單例也許能幫助到你。
爲何添加了 readResolve()方法就能夠了?ObjectInputStream源碼中,讀取文件時寫死判斷是否有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) { EnumSingleton instance = EnumSingleton.getInstance(); instance.setData(new Object()); try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); System.out.println(c); Object o = c.newInstance(); System.out.println(o); } catch (Exception e) { e.printStackTrace(); } } }
java.lang.Object@2acf57e3 java.lang.Object@2acf57e3 true
沒有作任何處理,咱們發現運行結果和預期的同樣。那麼枚舉式單例模式如此神奇,它的神祕之處
在哪裏體現呢?下面經過分析源碼來揭開它的神祕面紗。
下載一個很是好用的 Java反編譯工具 Jad(下載地址:https://varaneckas.com/jad/),解壓後
配置好環境變量(這裏不作詳細介紹),就可使用命令行調用了。找到工程所在的Class目錄,複製
EnumSingleton.class 所在的路徑,以下圖所示。
而後切換到命令行,切換到工程所在的Class目錄,輸入命令 jad 並在後面輸入複製好的路徑,在
Class 目錄下會多出一個 EnumSingleton.jad 文件。打開 EnumSingleton.jad 文件咱們驚奇地發現有
以下代碼:
static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); }
原來,枚舉式單例模式在靜態代碼塊中就給INSTANCE進行了賦值,是餓漢式單例模式的實現。至
此,咱們還能夠試想,序列化可否破壞枚舉式單例模式呢?不妨再來看一下 JDK 源碼,仍是回到
ObjectInputStream的readObject0()方法:
private Object readObject0(boolean unshared) throws IOException { ... case TC_ENUM: return checkResolve(readEnum(unshared)); ... }
咱們看到,在readObject0()中調用了readEnum()方法,來看readEnum()方法的代碼實現:
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 static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(); c.newInstance(); } catch (Exception e) { e.printStackTrace(); } }
運行結果以下圖所示。
<img src="https://gitee.com/woshiamiaojiang/image-hosting/raw/master/image-20200227191227392.png" alt="image-20200227191227392" style="zoom:50%;" />
結果中報的是 java.lang.NoSuchMethodException異常,意思是沒找到無參的構造方法。這時候,
咱們打開 java.lang.Enum的源碼,查看它的構造方法,只有一個protected類型的構造方法,代碼如
下:
protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
咱們再來作一個下面這樣的測試:
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(); } }
運行結果以下圖所示
<img src="https://gitee.com/woshiamiaojiang/image-hosting/raw/master/image-20200227191559008.png" alt="image-20200227191559008" style="zoom: 50%;" />
這時錯誤已經很是明顯了,「Cannot reflectively create enum objects」,即不能用反射來建立
枚舉類型。仍是習慣性地想來看看JDK源碼,進入Constructor的newInstance()方法:
@CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
從上述代碼能夠看到,在 newInstance()方法中作了強制性的判斷,若是修飾符是Modifier.ENUM
枚舉類型,則直接拋出異常。
到此爲止,咱們是否是已經很是清晰明瞭呢?枚舉式單例模式也是《EffectiveJava》書中推薦的一種單例模式實現寫法。JDK枚舉的語法特殊性及反射也爲枚舉保駕護航,讓枚舉式單例模式成爲一種比
較優雅的實現。
枚舉源碼
java.lang.Enum經過valueOf得到值
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); } Map<String, T> enumConstantDirectory() { if (enumConstantDirectory == null) { T[] universe = getEnumConstantsShared(); if (universe == null) throw new IllegalArgumentException( getName() + " is not an enum type"); Map<String, T> m = new HashMap<>(2 * universe.length); for (T constant : universe) m.put(((Enum<?>)constant).name(), constant); enumConstantDirectory = m; } return enumConstantDirectory; } private volatile transient Map<String, T> enumConstantDirectory = null;
枚舉模式的實例自然具備線程安全性,防止序列化與反射的特性。
有點像餓漢式單例。建立時就將常量存放在map容器中。
優勢:寫法優雅。加載時就建立對象。線程安全。
缺點:不能大批量建立對象,不然會形成浪費。spring中不能使用它。
結論:若是不是特別重的對象,建議使用枚舉單例模式,它是JVM自然的單例。
Spring改良枚舉寫出的改良方法:IOC容器
接下來看註冊式單例模式的另外一種寫法,即容器式單例模式,建立ContainerSingleton類:
public class ContainerSingleton { private ContainerSingleton(){} private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>(); public static Object getInstance(String className){ Object instance = null; if(!ioc.containsKey(className)){ try { instance = Class.forName(className).newInstance(); ioc.put(className, instance); }catch (Exception e){ e.printStackTrace(); } return instance; }else{ return ioc.get(className); } } }
測試
public class ContainerSingletonTest { public static void main(String[] args) { Object instance1 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo"); Object instance2 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo"); System.out.println(instance1 == instance2); } }
結果
true
容器式單例模式適用於實例很是多的狀況,便於管理。但它是非線程安全的。到此,註冊式單例模式介紹完畢。咱們再來看看Spring中的容器式單例模式的實現代碼:
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */ private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<String, BeanWrapper>(16); }
容器爲啥不能被反射破壞?秩序的維護者,創造了一個生態
最後贈送給你們一個彩蛋,講講線程單例實現 ThreadLocal。ThreadLocal 不能保證其建立的對象
是全局惟一的,可是能保證在單個線程中是惟一的,天生是線程安全的。下面來看代碼:
public class ThreadLocalSingleton { private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance = new ThreadLocal<ThreadLocalSingleton>(){ @Override protected 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()); Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("End"); } }
運行結果以下圖所示。
com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 End Thread-0:com.gupaoedu.vip.pattern.singleton.lazy.LazyDoubleCheckSingleton@551f86f1 Thread-1:com.gupaoedu.vip.pattern.singleton.lazy.LazyDoubleCheckSingleton@551f86f1
咱們發現,在主線程中不管調用多少次,獲取到的實例都是同一個,都在兩個子線程中分別獲取到
了不一樣的實例。那麼 ThreadLocal是如何實現這樣的效果的呢?咱們知道,單例模式爲了達到線程安全
的目的,會給方法上鎖,以時間換空間。ThreadLocal 將全部的對象所有放在 ThreadLocalMap 中,爲每一個線程都提供一個對象,其實是以空間換時間來實現線程隔離的。
不是線程做爲key,而是threadlocal自己。
ThreadLocal源碼
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
AbstractFactoryBean
public final T getObject() throws Exception { if (isSingleton()) { return (this.initialized ? this.singletonInstance : getEarlySingletonInstance()); } else { return createInstance(); } } private T getEarlySingletonInstance() throws Exception { Class[] ifcs = getEarlySingletonInterfaces(); if (ifcs == null) { throw new FactoryBeanNotInitializedException( getClass().getName() + " does not support circular references"); } if (this.earlySingletonInstance == null) { this.earlySingletonInstance = (T) Proxy.newProxyInstance( this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler()); } return this.earlySingletonInstance; }
MyBatis的ErrorContext使用了ThreadLocal
public class ErrorContext { private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>(); private ErrorContext() { } public static ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; } }
單例模式優勢:
單例模式的缺點:
學習單例模式的知識重點總結
單例模式能夠保證內存裏只有一個實例,減小了內存的開銷,還能夠避免對資源的多重佔用。單例模式看起來很是簡單,實現起來其實也很是簡單,可是在面試中倒是一個高頻面試點。但願「小夥伴們」
經過本章的學習,對單例模式有了很是深入的認識,在面試中彰顯技術深度,提高核心競爭力,給面試
加分,順利拿到錄取通知(Offer)。
一、解決容器式單例的線程安全問題。
兩種方法:雙重檢查鎖,利用ConcurrentHashMap#putIfAbsent()方法的原子性。
public class ContainerSingleton { private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>(); private ContainerSingleton() { throw new RuntimeException("不可被實例化!"); } // 方法一:雙重檢查鎖 public static Object getInstance(String className) { Object instance = null; if (!ioc.containsKey(className)) { synchronized (ContainerSingleton.class) { if (!ioc.containsKey(className)) { try { instance = Class.forName(className).newInstance(); ioc.put(className, instance); } catch (Exception e) { e.printStackTrace(); } return instance; } else { return ioc.get(className); } } } return ioc.get(className); } // 方法二:利用ConcurrentHashMap#putIfAbsent()方法的原子性 public static Object getInstance1(String className){ Object instance = null; try { ioc.putIfAbsent(className, Class.forName(className).newInstance()); }catch (Exception e){ e.printStackTrace(); } return ioc.get(className); } }