摘要:?jiǎn)卫J疥P(guān)注的重點(diǎn)私有構(gòu)造器線程安全延遲加載序列化和反序列化安全反射攻擊安全相關(guān)設(shè)計(jì)模式單例模式和工廠模式工廠類可以設(shè)計(jì)成單例模式。
0x01.定義與類型
定義:保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)全局訪問點(diǎn)
類型:創(chuàng)建型
UML
單例模式的基本要素
私有的構(gòu)造方法
指向自己實(shí)例的私有靜態(tài)引用
以自己實(shí)例為返回值的靜態(tài)的公有的方法
0x02.適用場(chǎng)景像確保任何情況下都絕對(duì)只有一個(gè)實(shí)例
需要頻繁實(shí)例化然后銷毀的對(duì)象。
創(chuàng)建對(duì)象時(shí)耗時(shí)過多或者耗資源過多,但又經(jīng)常用到的對(duì)象。
有狀態(tài)的工具類對(duì)象。
頻繁訪問數(shù)據(jù)庫(kù)或文件的對(duì)象。
0x03.單例模式的優(yōu)缺點(diǎn) 1.優(yōu)點(diǎn)在內(nèi)存里只有一個(gè)實(shí)例,減少了內(nèi)存開銷
可以避免對(duì)資源的多重占用
避免重復(fù)創(chuàng)建對(duì)象,提高性能
設(shè)置全局訪問點(diǎn),嚴(yán)格控制訪問
2.缺點(diǎn)沒有接口,擴(kuò)展困難
違反開閉原則
0x04.單例模式的幾種實(shí)現(xiàn)方式 1.餓漢式餓漢式:顧名思義,對(duì)象比較饑餓,所以一開始就創(chuàng)建好了。餓漢式也是單例模式的最簡(jiǎn)單實(shí)現(xiàn)。
Java實(shí)現(xiàn)
/** * 餓漢式 * 一開始就new好了 */ public class HungrySingleton implements Serializable { /** * 可以直接new也可以適用靜態(tài)塊中創(chuàng)建 * */ private final static HungrySingleton hungrySingleton; static { hungrySingleton = new HungrySingleton1(); } public static HungrySingleton getInstance() { return hungrySingleton; } /** * 私有構(gòu)造函數(shù) */ private HungrySingleton() {} }
餓漢式的單例模式,對(duì)象一開始就創(chuàng)建好了。不需要考慮線程安全問題。
餓漢式單例模式如果消耗資源比較多,而對(duì)象未被適用則會(huì)造成資源浪費(fèi)。
2.懶漢式懶漢式:說明類對(duì)象比較懶,沒有直接創(chuàng)建,而是延遲加載的,是第一次獲取對(duì)象的時(shí)候才創(chuàng)建。懶漢式的單例模式應(yīng)用較多。
a.第一個(gè)版本的Java實(shí)現(xiàn)(非線程安全)/** * 懶漢式 * 線程不安全 */ public class LazySingleton { private static LazySingleton lazySingleton = null; //線程不安全,當(dāng)有兩個(gè)線程同時(shí)創(chuàng)建對(duì)象,會(huì)違背單例模式 public static LazySingleton getInstance() { if (lazySingleton == null) { //會(huì)發(fā)生指令重排 lazySingleton = new LazySingleton(); } return lazySingleton; } private LazySingleton() {} }
這個(gè)版本的懶漢式會(huì)出現(xiàn)線程安全的問題,當(dāng)兩個(gè)線程同時(shí)訪問getInstance()靜態(tài)方法時(shí),lazySingleton還未創(chuàng)建,就會(huì)創(chuàng)建出兩個(gè)實(shí)例,違背了單例模式。
這里可以在getInstance()方法添加同步鎖synchronized解決,也可以在方法體添加類鎖,但是這樣相當(dāng)于完全鎖住了getInstance(),會(huì)出現(xiàn)性能問題。
推薦適用下面這種方式
b.雙重檢查鎖double check懶漢式(線程安全,通常適用這種方式)/** * 懶漢式 * 線程不安全 */ public class LazyDoubleCheckSingleton { //volatile 禁止指令重排序 private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; /** * 在靜態(tài)方法中直接加synchronized相當(dāng)于鎖了類 * @return */ public static LazyDoubleCheckSingleton getInstance() { //同樣實(shí)鎖類, 指令重排序 if (lazyDoubleCheckSingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazyDoubleCheckSingleton == null) { /** * 1.分配內(nèi)存給這個(gè)對(duì)象 * 2.初始化對(duì)象 * 3.設(shè)置lazyDoubleCheckSingleton指向剛分配的內(nèi)存 * 2 3 順序有可能發(fā)生顛倒 * intra-thread semantics 不會(huì)改變單線程執(zhí)行結(jié)果,指令重排序 */ lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); } } } return lazyDoubleCheckSingleton; } private LazyDoubleCheckSingleton() {} }
雙重檢查,只有對(duì)象為空的時(shí)候才會(huì)需要同步鎖,而第二次判斷是否為null,是對(duì)象是否已經(jīng)創(chuàng)建。
添加volatile關(guān)鍵字,防止指令重排序。
c.基于靜態(tài)內(nèi)部類的延遲加載方案私有靜態(tài)類的延遲加載
public class StaticInnerClassSingleton { /** * 看靜態(tài)類的初始化鎖那個(gè)線程可以拿到 */ 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("單例對(duì)象禁止反射調(diào)用"); } } }
將延遲初始化交給靜態(tài)類的初始化
3.容器單例使用靜態(tài)容器方式來(lái)實(shí)現(xiàn)多單例類
public class ContainerSingleton { //靜態(tài)容器, 注意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
通常使用容器單例情況是:?jiǎn)卫龑?duì)象比較多,需要統(tǒng)一維護(hù)。
4.枚舉單例模式(推薦使用)枚舉單例是從JVM層面上做的限制
public enum EnumInstance { /** * 具體的單例實(shí)例 */ 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; } }
后續(xù)會(huì)介紹到,單例模式完美防御了反射與序列化攻擊
5.ThreadLocal線程單例(并不是嚴(yán)格意義上的單例模式)有一部分場(chǎng)景,要求對(duì)象的生命周期隨著線程
/** * 線程級(jí)單例模式 */ public class ThreadLocalInstance { //靜態(tài)的ThreadLocal類保存對(duì)象 private static final ThreadLocalthreadLocal = ThreadLocal.withInitial(ThreadLocalInstance::new); private ThreadLocalInstance () {} public static ThreadLocalInstance getInstance () { return threadLocal.get(); } }
通過getInstance()獲取該線程的實(shí)例。
0x05.單例模式的序列化與反射攻擊 1.序列化攻擊以前面餓漢式舉例
測(cè)試代碼
public class SerializableTest { public static void main(String[] args) throws IOException, ClassNotFoundException { //1.實(shí)例化 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); } }
輸出結(jié)果
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; } /** * 寫完后,序列化對(duì)象會(huì)通過反射調(diào)用這個(gè)方法 * 完全是ObjectInputStream寫死的,并沒有任何繼承關(guān)系 * 其實(shí)每次序列化 反序列化 都已經(jīng)創(chuàng)建對(duì)象了,只是最后返回的這一個(gè) * @return */ private Object readResolve () { return hungrySingleton; } private HungrySingleton() {} }
輸出結(jié)果
org.ko.singleton.hungry.HungrySingleton@135fbaa4 org.ko.singleton.hungry.HungrySingleton@135fbaa4 true
為什么添加了readResolve()方法就可以了?
ObjectInputStream源碼中,讀取文件時(shí)寫死判斷是否有readResolve()方法,有調(diào)用這個(gè)方法,沒有則重新創(chuàng)建對(duì)象。
2.反射攻擊通過反射攻擊,實(shí)例化對(duì)象創(chuàng)建出第二個(gè)單例對(duì)象
/** * 類加載時(shí)就已經(jīng)創(chuàng)建好對(duì)象 */ 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); //反射創(chuàng)建 HungrySingleton instance = HungrySingleton.getInstance(); //正常創(chuàng)建 HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); //StaticInnerClassSingleton類也是一樣的 } }
測(cè)試結(jié)果
org.ko.singleton.hungry.HungrySingleton@1540e19d org.ko.singleton.hungry.HungrySingleton@677327b6 false
解決辦法:在構(gòu)造方法拋出異常
/** * 餓漢式 * 一開始就new好了 */ public class HungrySingleton implements Serializable { private final static HungrySingleton hungrySingleton; static { hungrySingleton = new HungrySingleton(); } public static HungrySingleton getInstance() { return hungrySingleton; } /** * 寫完后,序列化對(duì)象會(huì)通過反射調(diào)用這個(gè)方法 * 完全是ObjectInputStream寫死的,并沒有任何繼承關(guān)系 * 其實(shí)每次序列化 反序列化 都已經(jīng)創(chuàng)建對(duì)象了,只是最后返回的這一個(gè) * @return */ private Object readResolve () { return hungrySingleton; } private HungrySingleton() { /** * 對(duì)一開始就創(chuàng)建好了的類有效 */ if (hungrySingleton != null) { throw new RuntimeException("單例對(duì)象禁止反射調(diào)用"); } } }
再次測(cè)試輸出結(jié)果
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: 單例對(duì)象禁止反射調(diào)用 at org.ko.singleton.hungry.HungrySingleton2.(HungrySingleton2.java:36) ... 5 more
注意使用這種方式防止反射攻擊,餓漢式正常,懶漢式因?yàn)閯?chuàng)建對(duì)象的時(shí)機(jī)不同還是會(huì)出現(xiàn)問題,這種方式只能做到盡量的防御。
3.關(guān)于枚舉單例模式防止序列化與反射枚舉模式的實(shí)例天然具有線程安全性,防止序列化與反射的特性
驗(yàn)證代碼
/** * 枚舉類測(cè)試 */ public class SerializableTest { public static void main(String[] args) throws IOException, ClassNotFoundException { //測(cè)試枚舉類型 EnumInstance instance = EnumInstance.getInstance(); //設(shè)置對(duì)象 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(); //比較實(shí)例 System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); //比較實(shí)例中引用對(duì)象 System.out.println(instance.getData()); System.out.println(newInstance.getData()); System.out.println(instance.getData() == newInstance.getData()); } }
測(cè)試結(jié)果:
INSTANCE INSTANCE true java.lang.Object@5fd0d5ae java.lang.Object@5fd0d5ae true
反射攻擊測(cè)試
/** * 類加載時(shí)就已經(jīng)創(chuàng)建好對(duì)象 */ 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); //反射對(duì)象 EnumInstance newInstance = (EnumInstance) constructor.newInstance("K.O", 1); //實(shí)例對(duì)象 EnumInstance instance = EnumInstance.getInstance(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); } }
測(cè)試結(jié)果,枚舉類沒辦法通過構(gòu)造函數(shù)創(chuàng)建實(shí)例
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)
枚舉類反編譯結(jié)果
//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); } //私有構(gòu)造器 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[]; //通過靜態(tài)塊加載它,比較像餓漢模式 static { INSTANCE = new EnumInstance("INSTANCE", 0); $VALUES = (new EnumInstance[] { INSTANCE }); } }
結(jié)論:如果不是特別重的對(duì)象,建議使用枚舉單例模式,它是JVM天然的單例。
0x06.單例模式關(guān)注的重點(diǎn)私有構(gòu)造器
線程安全
延遲加載
序列化和反序列化安全
反射攻擊安全
0x07.相關(guān)設(shè)計(jì)模式單例模式和工廠模式:工廠類可以設(shè)計(jì)成單例模式。
單例模式和享元模式:可以通過享元模式來(lái)獲取單例對(duì)象
0x08.相關(guān)代碼單例模式:https://github.com/sigmako/design-pattern/tree/master/singleton
0x09.參考文章慕課網(wǎng)設(shè)計(jì)模式精講: https://coding.imooc.com/class/270.html
23種設(shè)計(jì)模式(1):?jiǎn)卫J?/b>: https://blog.csdn.net/zhengzhb/article/details/7331369
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/74875.html
摘要:用來(lái)指向已創(chuàng)建好的實(shí)例構(gòu)造函數(shù)為空注意這里是關(guān)鍵這是我們需要調(diào)用的方法把函數(shù)也定義為空,這樣就大功告成啦。 接上一篇大話PHP設(shè)計(jì)模式之單例模式 這一篇介紹一下升級(jí)版的單例模式,廢話不說先上代碼 不完美的單例模式 class singleMode { //用來(lái)指向已創(chuàng)建好的實(shí)例 public static $instance; //判斷是...
摘要:博主按每天一個(gè)設(shè)計(jì)模式旨在初步領(lǐng)會(huì)設(shè)計(jì)模式的精髓,目前采用靠這吃飯和純粹喜歡兩種語(yǔ)言實(shí)現(xiàn)。單例模式用途如果一個(gè)類負(fù)責(zé)連接數(shù)據(jù)庫(kù)的線程池日志記錄邏輯等等,此時(shí)需要單例模式來(lái)保證對(duì)象不被重復(fù)創(chuàng)建,以達(dá)到降低開銷的目的。 博主按:《每天一個(gè)設(shè)計(jì)模式》旨在初步領(lǐng)會(huì)設(shè)計(jì)模式的精髓,目前采用javascript(_靠這吃飯_)和python(_純粹喜歡_)兩種語(yǔ)言實(shí)現(xiàn)。誠(chéng)然,每種設(shè)計(jì)模式都有多種實(shí)...
摘要:博主按每天一個(gè)設(shè)計(jì)模式旨在初步領(lǐng)會(huì)設(shè)計(jì)模式的精髓,目前采用靠這吃飯和純粹喜歡兩種語(yǔ)言實(shí)現(xiàn)。單例模式用途如果一個(gè)類負(fù)責(zé)連接數(shù)據(jù)庫(kù)的線程池日志記錄邏輯等等,此時(shí)需要單例模式來(lái)保證對(duì)象不被重復(fù)創(chuàng)建,以達(dá)到降低開銷的目的。 博主按:《每天一個(gè)設(shè)計(jì)模式》旨在初步領(lǐng)會(huì)設(shè)計(jì)模式的精髓,目前采用javascript(_靠這吃飯_)和python(_純粹喜歡_)兩種語(yǔ)言實(shí)現(xiàn)。誠(chéng)然,每種設(shè)計(jì)模式都有多種實(shí)...
摘要:上面是簡(jiǎn)單的單例模式,自己寫程序的話夠用了,如果想繼續(xù)延伸,請(qǐng)傳送至大話設(shè)計(jì)模式之單例模式升級(jí)版 看了那么多單例的介紹,都是上來(lái)就說怎么做,也沒見說為什么這么做的。那小的就來(lái)說說為什么會(huì)有單例這個(gè)模式以便更好的幫助初學(xué)者真正的理解這個(gè)設(shè)計(jì)模式,如果你是大神,也不妨看完指正一下O(∩_∩)O首先我不得不吐槽一下這個(gè)模式名字單例,初學(xué)者通過字面很難理解什么是單例,我覺得應(yīng)該叫唯一模式更貼切...
摘要:最近開展了三次設(shè)計(jì)模式的公開課,現(xiàn)在來(lái)總結(jié)一下設(shè)計(jì)模式在中的應(yīng)用,這是第一篇?jiǎng)?chuàng)建型模式之單例模式。不過因?yàn)椴恢С侄嗑€程所以不需要考慮這個(gè)問題了。 最近開展了三次設(shè)計(jì)模式的公開課,現(xiàn)在來(lái)總結(jié)一下設(shè)計(jì)模式在PHP中的應(yīng)用,這是第一篇?jiǎng)?chuàng)建型模式之單例模式。 一、設(shè)計(jì)模式簡(jiǎn)介 首先我們來(lái)認(rèn)識(shí)一下什么是設(shè)計(jì)模式: 設(shè)計(jì)模式是一套被反復(fù)使用、容易被他人理解的、可靠的代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。 設(shè)計(jì)模式不...
摘要:原文博客地址單例模式系統(tǒng)中被唯一使用,一個(gè)類只有一個(gè)實(shí)例。中的單例模式利用閉包實(shí)現(xiàn)了私有變量?jī)烧呤欠裣嗟热躅愋停瑳]有私有方法,使用者還是可以直接一個(gè),也會(huì)有方法分割線不是單例最簡(jiǎn)單的單例模式,就是對(duì)象。 原文博客地址:https://finget.github.io/2018/11/06/single/ 單例模式 系統(tǒng)中被唯一使用,一個(gè)類只有一個(gè)實(shí)例。實(shí)現(xiàn)方法一般是先判斷實(shí)例是否存在,...
閱讀 3879·2021-09-22 10:57
閱讀 1988·2019-08-30 15:55
閱讀 2778·2019-08-30 15:44
閱讀 1810·2019-08-30 15:44
閱讀 1944·2019-08-30 15:44
閱讀 2326·2019-08-30 12:49
閱讀 1138·2019-08-29 18:47
閱讀 3218·2019-08-29 16:15