摘要:單例模式關注的重點私有構造器線程安全延遲加載序列化和反序列化安全反射攻擊安全相關設計模式單例模式和工廠模式工廠類可以設計成單例模式。
0x01.定義與類型
定義:保證一個類僅有一個實例,并提供一個全局訪問點
類型:創建型
UML
單例模式的基本要素
私有的構造方法
指向自己實例的私有靜態引用
以自己實例為返回值的靜態的公有的方法
0x02.適用場景像確保任何情況下都絕對只有一個實例
需要頻繁實例化然后銷毀的對象。
創建對象時耗時過多或者耗資源過多,但又經常用到的對象。
有狀態的工具類對象。
頻繁訪問數據庫或文件的對象。
0x03.單例模式的優缺點 1.優點在內存里只有一個實例,減少了內存開銷
可以避免對資源的多重占用
避免重復創建對象,提高性能
設置全局訪問點,嚴格控制訪問
2.缺點沒有接口,擴展困難
違反開閉原則
0x04.單例模式的幾種實現方式 1.餓漢式餓漢式:顧名思義,對象比較饑餓,所以一開始就創建好了。餓漢式也是單例模式的最簡單實現。
Java實現
/** * 餓漢式 * 一開始就new好了 */ public class HungrySingleton implements Serializable { /** * 可以直接new也可以適用靜態塊中創建 * */ private final static HungrySingleton hungrySingleton; static { hungrySingleton = new HungrySingleton1(); } public static HungrySingleton getInstance() { return hungrySingleton; } /** * 私有構造函數 */ private HungrySingleton() {} }
餓漢式的單例模式,對象一開始就創建好了。不需要考慮線程安全問題。
餓漢式單例模式如果消耗資源比較多,而對象未被適用則會造成資源浪費。
2.懶漢式懶漢式:說明類對象比較懶,沒有直接創建,而是延遲加載的,是第一次獲取對象的時候才創建。懶漢式的單例模式應用較多。
a.第一個版本的Java實現(非線程安全)/** * 懶漢式 * 線程不安全 */ public class LazySingleton { private static LazySingleton lazySingleton = null; //線程不安全,當有兩個線程同時創建對象,會違背單例模式 public static LazySingleton getInstance() { if (lazySingleton == null) { //會發生指令重排 lazySingleton = new LazySingleton(); } return lazySingleton; } private LazySingleton() {} }
這個版本的懶漢式會出現線程安全的問題,當兩個線程同時訪問getInstance()靜態方法時,lazySingleton還未創建,就會創建出兩個實例,違背了單例模式。
這里可以在getInstance()方法添加同步鎖synchronized解決,也可以在方法體添加類鎖,但是這樣相當于完全鎖住了getInstance(),會出現性能問題。
推薦適用下面這種方式
b.雙重檢查鎖double check懶漢式(線程安全,通常適用這種方式)/** * 懶漢式 * 線程不安全 */ public class LazyDoubleCheckSingleton { //volatile 禁止指令重排序 private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; /** * 在靜態方法中直接加synchronized相當于鎖了類 * @return */ public static LazyDoubleCheckSingleton getInstance() { //同樣實鎖類, 指令重排序 if (lazyDoubleCheckSingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazyDoubleCheckSingleton == null) { /** * 1.分配內存給這個對象 * 2.初始化對象 * 3.設置lazyDoubleCheckSingleton指向剛分配的內存 * 2 3 順序有可能發生顛倒 * intra-thread semantics 不會改變單線程執行結果,指令重排序 */ lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); } } } return lazyDoubleCheckSingleton; } private LazyDoubleCheckSingleton() {} }
雙重檢查,只有對象為空的時候才會需要同步鎖,而第二次判斷是否為null,是對象是否已經創建。
添加volatile關鍵字,防止指令重排序。
c.基于靜態內部類的延遲加載方案私有靜態類的延遲加載
public class StaticInnerClassSingleton { /** * 看靜態類的初始化鎖那個線程可以拿到 */ private static class InnerClass { private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton getInstance() { return InnerClass.staticInnerClassSingleton; } private StaticInnerClassSingleton () { if (InnerClass.staticInnerClassSingleton != null) { throw new RuntimeException("單例對象禁止反射調用"); } } }
將延遲初始化交給靜態類的初始化
3.容器單例使用靜態容器方式來實現多單例類
public class ContainerSingleton { //靜態容器, 注意map不是線程安全的,如果為了線程安全可以使用HashTable或者ConcurrentHashMap private static MapsingletonMap = new HashMap<>(); public static void putInstance (String key, Object instance) { if (key != null && key.length() != 0) { if (!singletonMap.containsKey(key)) { singletonMap.put(key, instance); } } } public static Object getInstance (String key) { return singletonMap.get(key); } }
容器單例如果要保證線程安全性,建議使用ConcurrentHashMap
通常使用容器單例情況是:單例對象比較多,需要統一維護。
4.枚舉單例模式(推薦使用)枚舉單例是從JVM層面上做的限制
public enum EnumInstance { /** * 具體的單例實例 */ INSTANCE { protected void printTest () { System.out.println("K.O print Test!"); } }; private Object data; protected abstract void printTest(); public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumInstance getInstance() { return INSTANCE; } }
后續會介紹到,單例模式完美防御了反射與序列化攻擊
5.ThreadLocal線程單例(并不是嚴格意義上的單例模式)有一部分場景,要求對象的生命周期隨著線程
/** * 線程級單例模式 */ public class ThreadLocalInstance { //靜態的ThreadLocal類保存對象 private static final ThreadLocalthreadLocal = ThreadLocal.withInitial(ThreadLocalInstance::new); private ThreadLocalInstance () {} public static ThreadLocalInstance getInstance () { return threadLocal.get(); } }
通過getInstance()獲取該線程的實例。
0x05.單例模式的序列化與反射攻擊 1.序列化攻擊以前面餓漢式舉例
測試代碼
public class SerializableTest { public static void main(String[] args) throws IOException, ClassNotFoundException { //1.實例化 HungrySingleton instance = HungrySingleton.getInstance(); //2.寫入本地文件 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(instance); //3.讀取 File file = new File("singleton_file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); HungrySingleton newInstance = (HungrySingleton) ois.readObject(); //4.比較 System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); } }
輸出結果
org.ko.singleton.hungry.HungrySingleton@135fbaa4 org.ko.singleton.hungry.HungrySingleton@568db2f2 false
解決方案:添加readResolve()方法
修改后
/** * 餓漢式 * 一開始就new好了 */ public class HungrySingleton implements Serializable { private final static HungrySingleton hungrySingleton; static { hungrySingleton = new HungrySingleton(); } public static HungrySingleton getInstance() { return hungrySingleton; } /** * 寫完后,序列化對象會通過反射調用這個方法 * 完全是ObjectInputStream寫死的,并沒有任何繼承關系 * 其實每次序列化 反序列化 都已經創建對象了,只是最后返回的這一個 * @return */ private Object readResolve () { return hungrySingleton; } private HungrySingleton() {} }
輸出結果
org.ko.singleton.hungry.HungrySingleton@135fbaa4 org.ko.singleton.hungry.HungrySingleton@135fbaa4 true
為什么添加了readResolve()方法就可以了?
ObjectInputStream源碼中,讀取文件時寫死判斷是否有readResolve()方法,有調用這個方法,沒有則重新創建對象。
2.反射攻擊通過反射攻擊,實例化對象創建出第二個單例對象
/** * 類加載時就已經創建好對象 */ public class ReflectTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class objectClass = HungrySingleton.class; Constructor constructor = objectClass.getDeclaredConstructor(); constructor.setAccessible(true); //反射創建 HungrySingleton instance = HungrySingleton.getInstance(); //正常創建 HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); //StaticInnerClassSingleton類也是一樣的 } }
測試結果
org.ko.singleton.hungry.HungrySingleton@1540e19d org.ko.singleton.hungry.HungrySingleton@677327b6 false
解決辦法:在構造方法拋出異常
/** * 餓漢式 * 一開始就new好了 */ public class HungrySingleton implements Serializable { private final static HungrySingleton hungrySingleton; static { hungrySingleton = new HungrySingleton(); } public static HungrySingleton getInstance() { return hungrySingleton; } /** * 寫完后,序列化對象會通過反射調用這個方法 * 完全是ObjectInputStream寫死的,并沒有任何繼承關系 * 其實每次序列化 反序列化 都已經創建對象了,只是最后返回的這一個 * @return */ private Object readResolve () { return hungrySingleton; } private HungrySingleton() { /** * 對一開始就創建好了的類有效 */ if (hungrySingleton != 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 org.ko.singleton.ReflectTest1.main(ReflectTest1.java:23) Caused by: java.lang.RuntimeException: 單例對象禁止反射調用 at org.ko.singleton.hungry.HungrySingleton2.(HungrySingleton2.java:36) ... 5 more
注意使用這種方式防止反射攻擊,餓漢式正常,懶漢式因為創建對象的時機不同還是會出現問題,這種方式只能做到盡量的防御。
3.關于枚舉單例模式防止序列化與反射枚舉模式的實例天然具有線程安全性,防止序列化與反射的特性
驗證代碼
/** * 枚舉類測試 */ public class SerializableTest { public static void main(String[] args) throws IOException, ClassNotFoundException { //測試枚舉類型 EnumInstance instance = EnumInstance.getInstance(); //設置對象 instance.setData(new Object()); //寫入文件 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(instance); //讀取文件 File file = new File("singleton_file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); EnumInstance newInstance = (EnumInstance) ois.readObject(); //比較實例 System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); //比較實例中引用對象 System.out.println(instance.getData()); System.out.println(newInstance.getData()); System.out.println(instance.getData() == newInstance.getData()); } }
測試結果:
INSTANCE INSTANCE true java.lang.Object@5fd0d5ae java.lang.Object@5fd0d5ae true
反射攻擊測試
/** * 類加載時就已經創建好對象 */ public class ReflectTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class objectClass = EnumInstance.class; Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); //反射對象 EnumInstance newInstance = (EnumInstance) constructor.newInstance("K.O", 1); //實例對象 EnumInstance instance = EnumInstance.getInstance(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); } }
測試結果,枚舉類沒辦法通過構造函數創建實例
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at org.ko.singleton.ReflectTest3.main(ReflectTest3.java:21)
枚舉類反編譯結果
//final的 public final class EnumInstance extends Enum{ public static EnumInstance[] values(){ return (EnumInstance[])$VALUES.clone(); } public static EnumInstance valueOf(String name){ return (EnumInstance)Enum.valueOf(org/ko/singleton/byenum/EnumInstance, name); } //私有構造器 private EnumInstance(String s, int i){ super(s, i); } public Object getData(){ return data; } public void setData(Object data){ this.data = data; } public static EnumInstance getInstance(){ return INSTANCE; } //static final public static final EnumInstance INSTANCE; private Object data; private static final EnumInstance $VALUES[]; //通過靜態塊加載它,比較像餓漢模式 static { INSTANCE = new EnumInstance("INSTANCE", 0); $VALUES = (new EnumInstance[] { INSTANCE }); } }
結論:如果不是特別重的對象,建議使用枚舉單例模式,它是JVM天然的單例。
0x06.單例模式關注的重點私有構造器
線程安全
延遲加載
序列化和反序列化安全
反射攻擊安全
0x07.相關設計模式單例模式和工廠模式:工廠類可以設計成單例模式。
單例模式和享元模式:可以通過享元模式來獲取單例對象
0x08.相關代碼單例模式:https://github.com/sigmako/design-pattern/tree/master/singleton
0x09.參考文章慕課網設計模式精講: https://coding.imooc.com/class/270.html
23種設計模式(1):單例模式: https://blog.csdn.net/zhengzhb/article/details/7331369
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74875.html
摘要:用來指向已創建好的實例構造函數為空注意這里是關鍵這是我們需要調用的方法把函數也定義為空,這樣就大功告成啦。 接上一篇大話PHP設計模式之單例模式 這一篇介紹一下升級版的單例模式,廢話不說先上代碼 不完美的單例模式 class singleMode { //用來指向已創建好的實例 public static $instance; //判斷是...
摘要:博主按每天一個設計模式旨在初步領會設計模式的精髓,目前采用靠這吃飯和純粹喜歡兩種語言實現。單例模式用途如果一個類負責連接數據庫的線程池日志記錄邏輯等等,此時需要單例模式來保證對象不被重復創建,以達到降低開銷的目的。 博主按:《每天一個設計模式》旨在初步領會設計模式的精髓,目前采用javascript(_靠這吃飯_)和python(_純粹喜歡_)兩種語言實現。誠然,每種設計模式都有多種實...
摘要:博主按每天一個設計模式旨在初步領會設計模式的精髓,目前采用靠這吃飯和純粹喜歡兩種語言實現。單例模式用途如果一個類負責連接數據庫的線程池日志記錄邏輯等等,此時需要單例模式來保證對象不被重復創建,以達到降低開銷的目的。 博主按:《每天一個設計模式》旨在初步領會設計模式的精髓,目前采用javascript(_靠這吃飯_)和python(_純粹喜歡_)兩種語言實現。誠然,每種設計模式都有多種實...
摘要:上面是簡單的單例模式,自己寫程序的話夠用了,如果想繼續延伸,請傳送至大話設計模式之單例模式升級版 看了那么多單例的介紹,都是上來就說怎么做,也沒見說為什么這么做的。那小的就來說說為什么會有單例這個模式以便更好的幫助初學者真正的理解這個設計模式,如果你是大神,也不妨看完指正一下O(∩_∩)O首先我不得不吐槽一下這個模式名字單例,初學者通過字面很難理解什么是單例,我覺得應該叫唯一模式更貼切...
摘要:最近開展了三次設計模式的公開課,現在來總結一下設計模式在中的應用,這是第一篇創建型模式之單例模式。不過因為不支持多線程所以不需要考慮這個問題了。 最近開展了三次設計模式的公開課,現在來總結一下設計模式在PHP中的應用,這是第一篇創建型模式之單例模式。 一、設計模式簡介 首先我們來認識一下什么是設計模式: 設計模式是一套被反復使用、容易被他人理解的、可靠的代碼設計經驗的總結。 設計模式不...
摘要:原文博客地址單例模式系統中被唯一使用,一個類只有一個實例。中的單例模式利用閉包實現了私有變量兩者是否相等弱類型,沒有私有方法,使用者還是可以直接一個,也會有方法分割線不是單例最簡單的單例模式,就是對象。 原文博客地址:https://finget.github.io/2018/11/06/single/ 單例模式 系統中被唯一使用,一個類只有一個實例。實現方法一般是先判斷實例是否存在,...
閱讀 3725·2021-09-22 10:57
閱讀 1914·2019-08-30 15:55
閱讀 2699·2019-08-30 15:44
閱讀 1731·2019-08-30 15:44
閱讀 1876·2019-08-30 15:44
閱讀 2244·2019-08-30 12:49
閱讀 1053·2019-08-29 18:47
閱讀 3135·2019-08-29 16:15