1、單例模式應用場景
單例模式(Single Pattern)是指確保一個類在任何狀況下絕對只是一個實例,並提供一個全局的訪問點。 單例模式在現實生活中的應用也很普遍。例如國家總統、公司CEO、部門經理等。在java標準中,ServletContext、ServletContextConfig等;在Spring框架中ApplicationCotext;數據庫對應的鏈接池也都是單例形勢的。java
2、單例模式分類
2.1 餓漢式單例
餓漢式單例是在類加載的時候就當即初始化了,而且建立了單例對象。絕對的線程安全,在線程還沒出現之前就實例化了,不可能存在訪問安全問題。數據庫
優勢:沒有加任何的鎖,執行效率高,在用戶體驗上,比懶漢式更好。 缺點:類加載的時候就初始化了,無論用與不用都佔空間,浪費了內存,有可能佔着茅坑不拉屎。緩存
Spring中的IOC容器ApplicationContext自己就是典型的餓漢式單例。案例代碼:安全
public class HungrySingleton { /** * 先靜態後動態 * 先屬性後方法 * 先上後下 */ private static final HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return hungrySingleton; } }
還有一種寫法,是利用靜態代碼塊機制:app
public class HungrySingleton { private static final HungrySingleton hungrySingleton; static { hungrySingleton = new HungrySingleton(); } private HungrySingleton() {} private HungrySingleton getInstance() { return hungrySingleton; } }
這兩種寫法都很簡單,也很容易理解。餓漢式使用在單例對象較少的狀況下。 下面來看下性能更優的寫法。框架
2.2 懶漢式單例
懶漢式單例是指被外部調用的時候纔會進行加載。示例代碼:ide
public class LazySingleton { /** * 懶漢式單例 * 在外部須要使用的時候才進行實例化 */ private LazySingleton() {} private static LazySingleton lazySingleton = null; public static LazySingleton getInstance() { if(lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } }
編寫一個線程類ExectorThread:工具
public class ExectorThread implements Runnable { @Override public void run() { LazySingleton lazySingleton = LazySingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + lazySingleton); } public static void main(String[] args) { System.out.println("--------begin-------"); Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("--------end---------"); } }
查看main方法屢次運行的結果發現:源碼分析
有必定概率會出現建立兩個不一樣結果的狀況,意味着上門的單例建立代碼存在線程安全隱患。咱們經過對代碼進行debug調試,發現經過不斷切換線程,並觀測其內存狀態,發如今線程環境下LazySingleton被實例化了兩次。有時候咱們獲得的運行結果多是相同的兩個對象,其實是被後面的執行線程給覆蓋了,看到了一個假象,線程安全隱患依然存在。這樣咱們須要在線程安全的環境下運行懶漢單例代碼。給getIntance()方法加上Synchronized關鍵字,使這個方法變成線程同步方法:性能
public class LazySingleton { /** * 懶漢式單例 * 在外部須要使用的時候才進行實例化 */ private LazySingleton() {} private static LazySingleton lazySingleton = null; public synchronized static LazySingleton getInstance() { if(lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } }
添加synchronized關鍵字使用鎖,在線程數量比較多的狀況下,若是CPU分配壓力上升,會致使大批線程出現阻塞,從而致使程序運行性能大幅度降低。那麼,有木有一種更好的方式,既兼顧線程的安全性又提高程序性能呢?答案是確定的。咱們會使用雙重檢查鎖的單例模式:
public class LazyDoubleCheckSingletion { private volatile static LazyDoubleCheckSingletion lazy = null; private LazyDoubleCheckSingletion() {} public static LazyDoubleCheckSingletion getInstance() { if(lazy == null) { synchronized(LazyDoubleCheckSingletion.class) { if(lazy == null) { lazy = new LazyDoubleCheckSingletion(); } } } return lazy; } }
當第一個線程調用 getInstance()方法時,第二個線程也能夠調用getInstance()。當第一個線程執行到 synchronized 時會上鎖,第二個線程就會變成 MONITOR狀態,出現阻塞。此時,阻塞並非基於整個 LazySingleton 類的阻塞,而是在 getInstance()方法內部阻塞,只要邏輯不是太複雜,對於調用者而言感知不到。
可是,用到 synchronized關鍵字,總歸是要上鎖,對程序性能仍是存在必定影響的。難道就真的沒有更好的方案嗎?固然是有的。咱們能夠從類初始化角度來考慮,看下面的代碼,採用靜態內部類的方式:
public class LazyInnerClassSingleton { /** * 這種形式兼顧餓漢式的內存浪費,也兼顧 synchronized 性能問題 * 完美地屏蔽了這兩個缺點 */ //若是沒使用的話,內部類是不加載的 private LazyInnerClassSingleton() {} /** * 每個關鍵字都不是多餘的 * static 是爲了使單例的空間共享 * 保證這個方法不會被重寫,重載 * @return */ public static final LazyInnerClassSingleton getInstance() { //在返回結果之前,必定會先加載內部類 return LazyHolder.LAZY; } //默認不加載 private static class LazyHolder{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
這種形式兼顧餓漢式的內存浪費,也兼顧synchronized性能問題。內部類必定是要在方法調用以前初始化,巧妙地避免了線程安全問題。
- 反射破壞單例
上面一些介紹單例模式的構造方法除了加上private之外,沒有作任何處理。若是使用反射來調用其構造方法,而後再調用getInstance()方法,應該就會有兩個不一樣的實例。仍是以LazyInnerClassSingleton爲例:
public static void main(String[] args) { Class<?> clazz = LazyInnerClassSingleton.class; try { //經過反射機制拿到私有的構造方法 Constructor c = clazz.getDeclaredConstructor(null); //強制訪問 c.setAccessible(true); ////暴力初始化 Object o1 = c.newInstance(); //調用了兩次構造方法,至關於 new 了兩次 Object o2 = c.newInstance(); System.out.println(o1 == o2); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
運行結果是:
運行結果很顯然是建立了兩個不一樣的實例。如今咱們對其構造方法作一些限制,一旦出現重複建立實例,則直接拋出異常。來看優化後的代碼:
/** * 這種形式兼顧餓漢式的內存浪費,也兼顧 synchronized 性能問題 * 完美地屏蔽了這兩個缺點 */ //若是沒使用的話,內部類是不加載的 private LazyInnerClassSingleton() { if(LazyHolder.LAZY != null) { throw new RuntimeException("Multiple instances are not allowed to be created!"); } } /** * 每個關鍵字都不是多餘的 * static 是爲了使單例的空間共享 * 保證這個方法不會被重寫,重載 * @return */ public static final LazyInnerClassSingleton getInstance() { //在返回結果之前,必定會先加載內部類 return LazyHolder.LAZY; } //默認不加載 private static class LazyHolder{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); }
再次運行結果:
至此,史上最牛 B 的單例寫法便大功告成。
- 序列化破壞單例
當咱們將一個單例對象建立好後,有時候須要將對象序列化後寫入到磁盤,下次使用的時候再從磁盤中讀取到對象,反序列化爲內存對象。反序列化後的對象會從新分配內存,即從新建立。那麼若是序列化的目標的對象爲單例對象,就違背了單例模式的初衷,至關於破壞了單例,來看一下代碼:
public class SerializeSingleton implements Serializable { public static final SerializeSingleton INSTANCE = new SerializeSingleton(); private SerializeSingleton() {} public static SerializeSingleton getInstance() { return INSTANCE; } }
編寫測試代碼:
public static void main(String[] args) { SerializeSingleton s1 = null; SerializeSingleton s2 = SerializeSingleton.getInstance(); FileOutputStream fos = null; try { fos = new FileOutputStream("SerializeSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerializeSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (SerializeSingleton)ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
運行main方法結果:
運行結果能夠看出反序列化後的對象和手動建立的對象不一致,實例化了兩次,違背了單例的設計初衷。那麼,如何保證序列化的狀況下也可以實現單例呢? 其實很簡單,只須要增長readResolve() 方法便可。來看一下優化後的代碼:
public class SerializeSingleton implements Serializable { public static final SerializeSingleton INSTANCE = new SerializeSingleton(); private SerializeSingleton() {} public static SerializeSingleton getInstance() { return INSTANCE; } private Object readResolve() { return INSTANCE; } }
再次運行結果:
爲何要這麼寫呢?咱們來一塊兒看下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 { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0) { throw new OptionalDataException(remain); } else if (defaultDataEnd) { /* * Fix for 4360508: stream is currently at the end of a field * value block written via default serialization; since there * is no terminating TC_ENDBLOCKDATA tag, simulate * end-of-custom-data behavior explicitly. */ throw new OptionalDataException(true); } bin.setBlockDataMode(false); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; totalObjectRefs++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); // force header read throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException(true); } else { throw new StreamCorruptedException( "unexpected end of block data"); } default: throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
咱們在源碼中看到了TC_OBJECT中判斷,調用了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); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(passHandle, resolveEx); } if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); } handles.finish(passHandle); 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; }
發現調用了 ObjectStreamClass 的 isInstantiable()方法,而isInstantiable()裏面的代碼以下:
boolean isInstantiable() { requireInitialized(); return (cons != null); }
代碼看起來很簡單,就是判斷了如下構造方法是否爲空,構造方法不爲空就返回true。這樣意味着,只要有無參構造方法就會實例化。
這個時候,其實仍是沒找到爲何加上readResolve()方法就避免了單例被破壞的真正緣由。再回到ObjectInputStream的readOrdinaryObject()方法繼續往下看:
判斷無參構造方法是否存在以後,又調用了hasReadResolveMethod()方法,來看代碼:
boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }
邏輯很是簡單,就是判斷 readResolveMethod 是否爲空,不爲空就返回 true。那麼 readResolveMethod 是在哪裏賦值的呢?經過全局查找找到了賦值代碼在私有方法 ObjectStreamClass()方法中給 readResolveMethod 進行賦值,來看代碼:
readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);
上面的邏輯其實就是經過反射找到一個無參的 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()方法返回實例,解決了單例被破壞的問題。可是,咱們經過分析源碼以及調試,咱們能夠看到實際上實例化了兩次,只不過新建立的對象沒有被返回而已。那若是,建立對象的動做發生頻率增大,就意味着內存分配開銷也就隨之增大,難道真的就沒辦法從根本上解決問題嗎?下面咱們來註冊式單例也許能幫助到你。
2.3 註冊式單例
註冊式單例又稱登記式單例,就是將每個實例都登記到一個地方,使用惟一標識獲取實例。註冊的方式有兩種:一種爲容器緩存,一種爲枚舉登記。先來看下枚舉式單例的寫法,建立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; } }
編寫測試main方法;
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(); } }
運行結果是:
沒有作任何的處理,咱們發現運行的結果和咱們預期的同樣。那麼枚舉式單例如此神奇,它的神祕之處體如今哪呢?下面咱們就經過分析源碼來揭開它的神祕面紗。 咱們使用jad反編譯工具(https://varaneckas.com/jad/) 生成的EnumSingleton.jad文件,打開這個文件發現這一段代碼:
static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); }
發現枚舉單例在靜態模塊中就給INSTANCE進行了賦值,是餓漢式單例的實現。 咱們回想序列化可否破壞枚舉式單例呢?再回到以前的源碼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; }
發現枚舉類型實際上是經過類名和class對象找到一個惟一的枚舉對象。 所以,枚舉對象不可能被加載器加載屢次。那麼反射能破壞枚舉式單例嗎?來看一下測試代碼:
public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(); c.newInstance(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
運行結果:
報的是 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("Kevin", 123); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
運行結果是:
這時錯誤已經很是明顯了,告訴咱們 Cannot reflectively create enum objects,不能用反射來建立枚舉類型。仍是看下JDK源碼,看下Constructor類的newInstance()方法:
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枚舉類型,直接拋出異常。到此爲止,咱們應該很是清晰明瞭了。
枚舉類型單例也是《Effective Java》書中很是推薦的一種單例的實現寫法。在 JDK 枚舉的語法特殊性,以及反射也爲枚舉保駕護航,讓枚舉式單例成爲一種比較優雅的實現。
註冊式單例還有另一種寫法,容器緩存的寫法,建立ContainerSingleton類:
public class ContainerSingleton { private ContainerSingleton() {} private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>(); public static Object getBean(String className) { synchronized (ioc) { if(!ioc.containsKey(className)) { Object object = null; try { object = Class.forName(className).newInstance(); ioc.put(className, object); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return object; }else { return ioc.get(className); } } } }
容器式寫法適用於建立實例很是多的狀況,便於管理。可是,是非線程安全的。到此,註冊式單例介紹完畢。
來看看 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<>(16); }
2.4 ThreadLocal 線程單例
ThreadLocal不能保證其建立的對象是全局惟一的,可是能保證在單個線程中是惟一的,天生的線程安全。 下面來看下示例代碼:
public class ThreadLocalSingleton { private static final ThreadLocal<ThreadLocalSingleton> instance = new ThreadLocal<ThreadLocalSingleton>() { @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton() {} public static ThreadLocalSingleton getInstance() { return instance.get(); } }
寫一下測試代碼:
public static void main(String[] args) { System.out.println("begin"); 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"); }
運行結果:
在主線程 main 中不管調用多少次,獲取到的實例都是同一個,都在兩個子線程中分別獲取到了不一樣的實例。那麼ThreadLocal是若是實現這樣的效果的呢?咱們知道上面的單例模式爲了達到線程安全的目的,給方法上鎖,以時間換空間。 ThreadLocal將全部的對象所有放在ThreadLocalMap中,爲每一個線程都提供一個對象,其實是以空間換時間來實現線程間隔離的。
3、單例模式總結
單例模式能夠保證內存裏只有一個實例,減小了內存開銷;能夠避免對資源的多重佔用。單例模式看起來很是簡單,實現起來其實也很是簡單。