單例是最多見的設計模式之一,實現的方式很是多,同時須要注意的問題也很是多。html
本文主要內容:java
單例模式(Singleton Pattern):確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。單例模式是一種對象建立型模式。算法
單例模式有三個要點:spring
Singleton(單例):在單例類的內部實現只生成一個實例,同時它提供一個靜態的 getInstance()
工廠方法,讓客戶能夠訪問它的惟一實例;爲了防止在外部對其實例化,將其構造函數設計爲私有;在單例類內部定義了一個 Singleton
類型的靜態對象,做爲外部共享的惟一實例。設計模式
// 線程安全 public class Singleton { private final static Singleton INSTANCE = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return INSTANCE; } }
優勢:簡單,使用時沒有延遲;在類裝載時就完成實例化,天生的線程安全瀏覽器
缺點:沒有懶加載,啓動較慢;若是從始至終都沒使用過這個實例,則會形成內存的浪費。安全
// 線程安全 public class Singleton { private static Singleton instance; static { instance = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return instance; } }
將類實例化的過程放在了靜態代碼塊中,在類裝載的時執行靜態代碼塊中的代碼,初始化類的實例。優缺點同上。微信
// 線程不安全 public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
優勢:懶加載,啓動速度快、若是從始至終都沒使用過這個實例,則不會初始化該實力,可節約資源mybatis
缺點:多線程環境下線程不安全。if (singleton == null)
存在競態條件,可能會有多個線程同時進入 if 語句
,致使產生多個實例多線程
// 線程安全,效率低 public class Singleton { private static Singleton singleton; private Singleton() {} public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
優勢:解決了上一種實現方式的線程不安全問題
缺點:synchronized 對整個 getInstance()
方法都進行了同步,每次只有一個線程可以進入該方法,併發性能極差
// 線程安全 public class Singleton { // 注意:這裏有 volatile 關鍵字修飾 private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
優勢:線程安全;延遲加載;效率較高。
因爲 JVM 具備指令重排的特性,在多線程環境下可能出現 singleton 已經賦值但還沒初始化的狀況,致使一個線程得到尚未初始化的實例。volatile 關鍵字的做用:
// 線程安全 public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }
優勢:避免了線程不安全,延遲加載,效率高。
靜態內部類的方式利用了類裝載機制來保證線程安全,只有在第一次調用getInstance方法時,纔會裝載SingletonInstance內部類,完成Singleton的實例化,因此也有懶加載的效果。
加入參數 -verbose:class
能夠查看類加載順序
$ javac Singleton.java $ java -verbose:class Singleton
// 線程安全 public enum Singleton { INSTANCE; public void whateverMethod() { } }
優勢:經過JDK1.5中添加的枚舉來實現單例模式,寫法簡單,且不只能避免多線程同步問題,並且還能防止反序列化從新建立新的對象。
單例模式的目標是,任什麼時候候該類都只有惟一的一個對象。可是上面咱們寫的大部分單例模式都存在漏洞,被攻擊時會產生多個對象,破壞了單例模式。
經過Java的序列化機制來攻擊單例模式
public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; } public static void main(String[] args) throws IOException, ClassNotFoundException { HungrySingleton singleton = HungrySingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(singleton); // 序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file")); HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); // 反序列化 System.out.println(singleton); System.out.println(newSingleton); System.out.println(singleton == newSingleton); } }
結果
com.singleton.HungrySingleton@ed17bee com.singleton.HungrySingleton@46f5f779 false
Java 序列化是如何攻擊單例模式的呢?咱們須要先複習一下Java的序列化機制
java.io.ObjectOutputStream
是Java實現序列化的關鍵類,它能夠將一個對象轉換成二進制流,而後能夠經過 ObjectInputStream
將二進制流還原成對象。具體的序列化過程不是本文的重點,在此僅列出幾個要點。
Java 序列化機制的要點:
java.io.Serializable
接口,不然會拋出NotSerializableException
異常serialVersionUID
變量,Java序列化機制會根據編譯時的class自動生成一個serialVersionUID
做爲序列化版本比較(驗證一致性),若是檢測到反序列化後的類的serialVersionUID
和對象二進制流的serialVersionUID
不一樣,則會拋出異常java.io.Serializable
接口transient
後,默認序列化機制就會忽略該字段,反序列化後自動得到0或者null值readObject
、writeObject
方法實現本身的序列化策略,即便是transient
修飾的成員變量也能夠手動調用ObjectOutputStream
的writeInt
等方法將這個成員變量序列化。private Object readResolve()
方法,在調用readObject
方法以後,若是存在readResolve
方法則自動調用該方法,readResolve
將對readObject
的結果進行處理,而最終readResolve
的處理結果將做爲readObject
的結果返回。readResolve
的目的是保護性恢復對象,其最重要的應用就是保護性恢復單例、枚舉類型的對象Serializable
接口是一個標記接口,可自動實現序列化,而Externalizable
繼承自Serializable
,它強制必須手動實現序列化和反序列化算法,相對來講更加高效根據上面對Java序列化機制的複習,咱們能夠自定義一個 readResolve
,在其中返回類的單例對象,替換掉 readObject
方法反序列化生成的對象,讓咱們本身寫的單例模式實現保護性恢復對象
public class HungrySingleton implements Serializable { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; } private Object readResolve() { return instance; } public static void main(String[] args) throws IOException, ClassNotFoundException { HungrySingleton singleton = HungrySingleton.getInstance(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file")); HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); System.out.println(singleton); System.out.println(newSingleton); System.out.println(singleton == newSingleton); } }
再次運行
com.singleton.HungrySingleton@24273305 com.singleton.HungrySingleton@24273305 true
注意:本身實現的單例模式都須要避免被序列化破壞
在單例模式中,構造器都是私有的,而反射能夠經過構造器對象調用 setAccessible(true)
來得到權限,這樣就能夠建立多個對象,來破壞單例模式了
public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { HungrySingleton instance = HungrySingleton.getInstance(); Constructor constructor = HungrySingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); // 得到權限 HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); } }
輸出結果
com.singleton.HungrySingleton@3b192d32 com.singleton.HungrySingleton@16f65612 false
反射是經過它的Class對象來調用構造器建立新的對象,咱們只須要在構造器中檢測並拋出異常就能夠達到目的了
private HungrySingleton() { // instance 不爲空,說明單例對象已經存在 if (instance != null) { throw new RuntimeException("單例模式禁止反射調用!"); } }
運行結果
Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.singleton.HungrySingleton.main(HungrySingleton.java:32) Caused by: java.lang.RuntimeException: 單例模式禁止反射調用! at com.singleton.HungrySingleton.<init>(HungrySingleton.java:20) ... 5 more
注意,上述方法針對餓漢式單例模式是有效的,但對懶漢式的單例模式是無效的,懶漢式的單例模式是沒法避免反射攻擊的!
爲何對餓漢有效,對懶漢無效?由於餓漢的初始化是在類加載的時候,反射必定是在餓漢初始化以後才能使用;而懶漢是在第一次調用 getInstance()
方法的時候才初始化,咱們沒法控制反射和懶漢初始化的前後順序,若是反射在前,無論反射建立了多少對象,instance都將一直爲null,直到調用 getInstance()
。
事實上,實現單例模式的惟一推薦方法,是使用枚舉類來實現。
寫下咱們的枚舉單例模式
package com.singleton; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public enum SerEnumSingleton implements Serializable { INSTANCE; // 單例對象 private String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; } private SerEnumSingleton() { } public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { SerEnumSingleton singleton1 = SerEnumSingleton.INSTANCE; singleton1.setContent("枚舉單例序列化"); System.out.println("枚舉序列化前讀取其中的內容:" + singleton1.getContent()); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj")); oos.writeObject(singleton1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerEnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); SerEnumSingleton singleton2 = (SerEnumSingleton) ois.readObject(); ois.close(); System.out.println(singleton1 + "\n" + singleton2); System.out.println("枚舉序列化後讀取其中的內容:" + singleton2.getContent()); System.out.println("枚舉序列化先後兩個是否同一個:" + (singleton1 == singleton2)); Constructor<SerEnumSingleton> constructor = SerEnumSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); SerEnumSingleton singleton3 = constructor.newInstance(); // 經過反射建立對象 System.out.println("反射後讀取其中的內容:" + singleton3.getContent()); System.out.println("反射先後兩個是否同一個:" + (singleton1 == singleton3)); } }
運行結果,序列化先後的對象是同一個對象,而反射的時候拋出了異常
枚舉序列化前讀取其中的內容:枚舉單例序列化 INSTANCE INSTANCE 枚舉序列化後讀取其中的內容:枚舉單例序列化 枚舉序列化先後兩個是否同一個:true Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.SerEnumSingleton.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at com.singleton.SerEnumSingleton.main(SerEnumSingleton.java:39)
編譯後,再經過 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: SerEnumSingleton.java package com.singleton; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public final class SerEnumSingleton extends Enum implements Serializable { public static SerEnumSingleton[] values() { return (SerEnumSingleton[])$VALUES.clone(); } public static SerEnumSingleton valueOf(String name) { return (SerEnumSingleton)Enum.valueOf(com/singleton/SerEnumSingleton, name); } public String getContent() { return content; } public void setContent(String content) { this.content = content; } private SerEnumSingleton(String s, int i) { super(s, i); } public static void main(String args[]) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { SerEnumSingleton singleton1 = INSTANCE; singleton1.setContent("\u679A\u4E3E\u5355\u4F8B\u5E8F\u5217\u5316"); System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton1.getContent()).toString()); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj")); oos.writeObject(singleton1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerEnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); SerEnumSingleton singleton2 = (SerEnumSingleton)ois.readObject(); ois.close(); System.out.println((new StringBuilder()).append(singleton1).append("\n").append(singleton2).toString()); System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton2.getContent()).toString()); System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton2).toString()); Constructor constructor = com/singleton/SerEnumSingleton.getDeclaredConstructor(new Class[0]); constructor.setAccessible(true); SerEnumSingleton singleton3 = (SerEnumSingleton)constructor.newInstance(new Object[0]); System.out.println((new StringBuilder()).append("\u53CD\u5C04\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton3.getContent()).toString()); System.out.println((new StringBuilder()).append("\u53CD\u5C04\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton3).toString()); } public static final SerEnumSingleton INSTANCE; private String content; private static final SerEnumSingleton $VALUES[]; static { INSTANCE = new SerEnumSingleton("INSTANCE", 0); $VALUES = (new SerEnumSingleton[] { INSTANCE }); } }
經過反編譯後代碼咱們能夠看到,ublic final class T extends Enum
,說明,當咱們使用enmu來定義一個枚舉類型的時候,編譯器會自動幫咱們建立一個final類型的類繼承Enum類,因此枚舉類型不能被繼承。
1. 枚舉單例寫法簡單
2. 線程安全&懶加載
代碼中 INSTANCE 變量被 public static final
修飾,由於static類型的屬性是在類加載以後初始化的,JVM能夠保證線程安全;且Java類是在引用到的時候才進行類加載,因此枚舉單例也有懶加載的效果。
3. 枚舉本身能避免序列化攻擊
爲了保證枚舉類型像Java規範中所說的那樣,每個枚舉類型極其定義的枚舉變量在JVM中都是惟一的,在枚舉類型的序列化和反序列化上,Java作了特殊的規定。
在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是經過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不容許任何對這種序列化機制的定製,所以禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 咱們看一下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); }
從代碼中能夠看到,代碼會嘗試從調用enumType這個Class對象的enumConstantDirectory()
方法返回的map中獲取名字爲name的枚舉對象,若是不存在就會拋出異常。再進一步跟到enumConstantDirectory()
方法,就會發現到最後會以反射的方式調用enumType這個類型的values()靜態方法,也就是上面咱們看到的編譯器爲咱們建立的那個方法,而後用返回結果填充enumType這個Class對象中的enumConstantDirectory
屬性。因此,JVM對序列化有保證。
4. 枚舉可以避免反射攻擊,由於反射不支持建立枚舉對象
Constructor
類的 newInstance
方法中會判斷是否爲 enum,如果會拋出異常
@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); } } // 不能爲 ENUM,不然拋出異常:不能經過反射建立 enum 對象 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; }
單例模式做爲一種目標明確、結構簡單、理解容易的設計模式,在軟件開發中使用頻率至關高,在不少應用軟件和框架中都得以普遍應用。
JDK Runtime類表明着Java程序的運行時環境,每一個Java程序都有一個Runtime實例,該類會被自動建立,咱們能夠經過 Runtime.getRuntime() 方法來獲取當前程序的Runtime實例。一旦獲得了一個當前的Runtime對象的引用,就能夠調用Runtime對象的方法去控制Java虛擬機的狀態和行爲。
Runtime 應用了餓漢式單例模式
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } private Runtime() { } //.... }
API 介紹
addShutdownHook(Thread hook) 註冊新的虛擬機來關閉掛鉤。 availableProcessors() 向 Java 虛擬機返回可用處理器的數目。 exec(String command) 在單獨的進程中執行指定的字符串命令。 exec(String[] cmdarray) 在單獨的進程中執行指定命令和變量。 exec(String[] cmdarray, String[] envp) 在指定環境的獨立進程中執行指定命令和變量。 exec(String[] cmdarray, String[] envp, File dir) 在指定環境和工做目錄的獨立進程中執行指定的命令和變量。 exec(String command, String[] envp) 在指定環境的單獨進程中執行指定的字符串命令。 exec(String command, String[] envp, File dir) 在有指定環境和工做目錄的獨立進程中執行指定的字符串命令。 exit(int status) 經過啓動虛擬機的關閉序列,終止當前正在運行的 Java 虛擬機。 freeMemory() 返回 Java 虛擬機中的空閒內存量。 gc() 運行垃圾回收器。 getRuntime() 返回與當前 Java 應用程序相關的運行時對象。 halt(int status) 強行終止目前正在運行的 Java 虛擬機。 load(String filename) 加載做爲動態庫的指定文件名。 loadLibrary(String libname) 加載具備指定庫名的動態庫。 maxMemory() 返回 Java 虛擬機試圖使用的最大內存量。 removeShutdownHook(Thread hook) 取消註冊某個先前已註冊的虛擬機關閉掛鉤。 runFinalization() 運行掛起 finalization 的全部對象的終止方法。 totalMemory() 返回 Java 虛擬機中的內存總量。 traceInstructions(on) 啓用/禁用指令跟蹤。 traceMethodCalls(on) 啓用/禁用方法調用跟蹤。
Desktop 類容許 Java 應用程序啓動已在本機桌面上註冊的關聯應用程序,以處理 URI 或文件。支持的操做包括:
Desktop 經過一個容器來管理單例對象
public class Desktop { // synchronized 同步方法 public static synchronized Desktop getDesktop(){ if (GraphicsEnvironment.isHeadless()) throw new HeadlessException(); if (!Desktop.isDesktopSupported()) { throw new UnsupportedOperationException("Desktop API is not " + "supported on the current platform"); } sun.awt.AppContext context = sun.awt.AppContext.getAppContext(); Desktop desktop = (Desktop)context.get(Desktop.class); // 獲取單例對象 // 存在則返回,不存在則建立,建立後put進容器 if (desktop == null) { desktop = new Desktop(); context.put(Desktop.class, desktop); } return desktop; }
AppContext 中有一個 HashMap 對象table,是實際的容器對象
private final Map<Object, Object> table = new HashMap();
AbstractFactoryBean 類
public final T getObject() throws Exception { if (this.isSingleton()) { return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance(); } else { return this.createInstance(); } } private T getEarlySingletonInstance() throws Exception { Class<?>[] ifcs = this.getEarlySingletonInterfaces(); if (ifcs == null) { throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references"); } else { if (this.earlySingletonInstance == null) { // 經過代理建立對象 this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler()); } return this.earlySingletonInstance; } }
ErrorContext 類,經過 ThreadLocal 管理單例對象,一個線程一個ErrorContext對象,ThreadLocal能夠保證線程安全
public class ErrorContext { private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>(); private ErrorContext() { } public static ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; } //... }
參考:
http://www.hollischuang.com/archives/197
http://www.javashuo.com/article/p-ozksreer-gs.html
https://blog.csdn.net/abc123lzf/article/details/82318148
歡迎評論、轉發、分享,您的支持是我最大的動力
更多內容可訪問個人我的博客:http://laijianfeng.org
關注【小旋鋒】微信公衆號,及時接收博文推送