Java設計模式(4 / 23):單例模式

文章目錄

  • 單例模式的應用場景
  • 餓漢式單例模式
  • 懶漢式單例模式
    • 改進: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中,為每個線程都提供一個對象,實際上是以空間換時間來實現線程隔離的。

單例模式小結

單例模式可以保證內存里只有一個實例,減少了內存的開銷,還可以避免對資源的多重占用。單例模式看起來非常簡單,實現起來其實也非常簡單,但是在面試中卻是一個高頻面試點。

參考資料

  1. 《Spring5核心原理與30個類手寫實戰》

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/445581.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/445581.shtml
英文地址,請注明出處:http://en.pswp.cn/news/445581.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

約瑟夫環-(數組、循環鏈表、數學)

約瑟夫環&#xff08;約瑟夫問題&#xff09;是一個數學的應用問題&#xff1a;已知n個人&#xff08;以編號1&#xff0c;2&#xff0c;3...n分別表示&#xff09;圍坐在一張圓桌周圍。從編號為k的人開始報數&#xff0c;數到m的那個人出列&#xff1b;他的下一個人又從1開始報…

Ubuntu麒麟下搭建FTP服務

一.怎么搭建FTP服務&#xff1a; 第一步>>更新庫 linuxidclinuxidc:~$ sudo apt-get update 第二步>>采用如下命令安裝VSFTPD的包 linuxidclinuxidc:~$ sudo apt-get install vsftpd 第三步>>安裝完成后打開 /etc/vsftpd.conf 文件&#xff0c;按如下所述…

《數據結構上機實驗(C語言實現)》筆記(1 / 12):緒論

文章目錄驗證性實驗求1~n的連續整數和說明放碼結果常見算法時間函數的增長趨勢分析說明放碼結果設計性實驗求素數個數說明放碼結果求連續整數階乘的和說明放碼結果驗證性實驗 求1~n的連續整數和 說明 對于給定的正整數n&#xff0c;求12…n12…n12…n&#xff0c;采用逐個累…

線性表實現一元多項式操作

數組存放&#xff1a; 不需要記錄冪&#xff0c;下標就是。 比如1&#xff0c;2&#xff0c;3&#xff0c;5表示12x3x^25x^3 有了思路&#xff0c;我們很容易定義結構 typedef struct node{float * coef;//系數數組int maxSize;//最大容量int order;//最高階數 }Polynomial…

ubuntu下解壓縮zip,tar,tar.gz和tar.bz2文件

在Linux下面如何去壓縮文件或者目錄呢&#xff1f; 在這里我們將學習zip, tar, tar.gz和tar.bz2等壓縮格式的基本用法。 首先了解下Linux里面常用的壓縮格式。 在我們探究這些用法之前&#xff0c;我想先跟大家分享一下使用不同壓縮格式的經驗。當然&#xff0c;我這里講到的只…

《數據結構上機實驗(C語言實現)》筆記(2 / 12):線性表

文章目錄驗證性實驗實現順序表各種基本運算的算法放碼sqlist.hsqlist.cppexp2-1.cpp結果實現單鏈表各種基本運算的算法放碼linklist.hlinklist.cppexp2-2.cpp結果實現雙鏈表各種基本運算的算法放碼dlinklist.hdlinklist.cppexp2-3.cpp結果實現循環單鏈表各種基本運算的算法放碼…

鏈表排序-歸并

鏈表排序&#xff0c;可以插入排序&#xff0c;我就不寫了。 實現歸并排序 歸并排序&#xff08;MERGE-SORT&#xff09;是建立在歸并操作上的一種有效的排序算法,該算法是采用分治法&#xff08;Divide and Conquer&#xff09;的一個非常典型的應用。將已有序的子序列合并&…

ubuntu麒麟下安裝并啟用搜狗輸入法

1.首先打開UK軟件&#xff0c;輸入搜狗尋找搜狗拼音軟件 然后下載搜狗拼音軟件 接著點擊啟動該軟件 2.點擊搜狗拼音的圖標&#xff0c;進入搜狗拼音的設置窗口 點擊高級&#xff0c;并打開FCITX設置 加入英語輸入法 3.這樣就可以進行中英文切換了

線性表表示集合

集合我們高中都學過吧&#xff1f; 最重要的幾個特點&#xff1a;元素不能重復、各個元素之間沒有關系、沒有順序 集合內的元素可以是單元素或者是集合。 對集合的操作&#xff1a;交集并集差集等&#xff0c;還有對自身的加減等。 需要頻繁的加減元素&#xff0c;所以順序…

家用無線路由器購買入門指南

視頻一&#xff1a;「白問」普通大眾 買路由器關注這幾個點就夠了 來源 例如商品名&#xff1a;AC 1200M 雙頻 AX前綴wifi6IEEE 802.11 AX AC前綴wifi5IEEE 802.11 AC AX比AC好 1200M 理論峰值 和網速無關 商家噱頭 MIMO SU-MIMO 單用戶多進多出&#xff08;早期&#xff…

ubuntu linux下執行.sh文件

ubuntu linux下執行.sh文件 首先&#xff0c;要確保這個文件的類型是可執行的。 有兩種辦法把文件設置為可執行文件。 1) 直接在文件屬性標簽中選中 "可執行"&#xff0c;--b 如果用的是圖形界面&#xff0c;這個方法最簡單直接。 2) 使用命令 chmod x file.sh。將可…

鏈表相交問題

本來想自己寫&#xff0c;寫了一半發現一篇文章&#xff0c;解釋寫得簡單易懂&#xff0c;我就直接拿過來了。 這個問題值得反復地寫&#xff0c;鍛煉鏈表coding能力的好題。 //如果兩個鏈表都不帶環 int NotCycleCheckCross(pLinkNode head1,pLinkNode head2) {pLinkNode lis…

用JS寫了一個模擬串行加法器

在重溫《編碼&#xff1a;隱匿在計算機軟硬件背后的語言》第12章——二進制加法器時&#xff0c;心血來潮用JS寫了一個模擬串行加法器。 測試斷言工具TestUtils.js function assertTrue(actual){if(!actual)throw "Error actual: " actual " is not true.&q…

Android學習路線總結

title: Android學習路線總結&#xff0c;絕對干貨 tags: Android學習路線,Android學習資料,怎么學習android grammar_cjkRuby: true --- 一、前言 不知不覺自己已經做了幾年開發了&#xff0c;由記得剛出來工作的時候感覺自己能牛X&#xff0c;現在回想起來感覺好無知。懂的越…

雙棧

利用棧底位置相對不變的特性&#xff0c;可以讓兩個順序棧共享一個空間。 具體實現方法大概有兩種&#xff1a; 一種是奇偶棧&#xff0c;就是所有下標為奇數的是一個棧&#xff0c;偶數是另一個棧。但是這樣一個棧的最大存儲就確定了&#xff0c;并沒有起到互補空缺的作用&a…

Error when loading the SDK:解決方案

錯誤情況&#xff1a; 當打開eclipse時出現如下窗口&#xff08;內容如下&#xff09; Error when loading the SDK: Error: Error parsing \Android\adt-bundle-windows-x86_64-20140702\sdk\system-images\android-22\android-wear\armeabi-v7a\devices.xml cvc-complex-type…

單調隊列優化的背包問題

對于背包問題&#xff0c;經典的背包九講已經講的很明白了&#xff0c;本來就不打算寫這方面問題了。 但是吧。 我發現&#xff0c;那個最出名的九講竟然沒寫隊列優化的背包。。。。 那我必須寫一下咯嘿嘿&#xff0c;這么好的思想。 我們回顧一下背包問題吧。 01背包問題 …

用Python去除掃描型PDF中的水印

內容概述 含水印掃描型PDF文件&#xff0c;其中某頁如下圖所示&#xff0c;用Python去除其頁頂及頁底的水印。 處理思路&#xff1a;PDF中的每一頁的水印的相對位置基本相同&#xff0c;將PDF每一頁輸出成圖片&#xff0c;然后進行圖片編輯&#xff0c;用白色填充方形覆蓋水印…

鏈表實現棧

棧&#xff0c;是操作受限的線性表&#xff0c;只能在一端進行插入刪除。 其實就是帶尾指針的鏈表&#xff0c;尾插 #include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #define Status int #define SElemType int //只在頭部進行插入和刪除(…

二階有源濾波器

濾波器是一種使用信號通過而同時抑制無用頻率信號的電子裝置, 在信息處理、數據傳送和抑制干擾等自動控制、通信及其它電子系統中應用廣泛。濾波一般可分為有源濾波和無源濾波, 有源濾波可以使幅頻特性比較陡峭, 而無源濾波設計簡單易行, 但幅頻特性不如濾波器, 而且體積較大。…