【Java设计模式】GOF32 - 单例模式
思維和思考的方式才是最重要的,語言只是一種工具。
GOF23
Group of Four——國外的四個軟件大牛總結出來的模式。
創建型模式:幫助我們創建對象
單例模式
單例模式的常見使用場景:
單例模式保證一個類只有一個示例,并且提供一個訪問該實例的全局訪問點。
由于單例模式只生成一個實例,減少了系統性能開銷
常見的五種單例模式實現方式
– 主要:
? 餓漢式(線程安全,調用效率高。 但是,不能延時加載。)
? 懶漢式(線程安全,調用效率不高。 但是,可以延時加載。)
– 其他:
? 雙重檢測鎖式(由于JVM底層內部模型原因,偶爾會出問題。不建議使用)
? 靜態內部類式(線程安全,調用效率高。 但是,可以延時加載)
? 枚舉式(線程安全,調用效率高,不能延時加載。并且可以天然的防止反射和反序列化漏洞!)
如何選用?
– 單例對象 占用 資源 少,不需要 延時加載:
? 枚舉式 好于 餓漢式
– 單例對象 占用 資源 大,需要 延時加載:
? 靜態內部類式 好于 懶漢式
常見應用場景
– Windows的Task Manager(任務管理器)就是很典型的單例模式
– windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統運行過程中,回收站一直維護著僅有的一個實例。
– 項目中,讀取配置文件的類,一般也只有一個對象。沒有必要每次使用配置文件數據,每次new一個對象去讀取。
– 網站的計數器,一般也是采用單例模式實現,否則難以同步。
– 應用程序的日志應用,一般都何用單例模式實現,這一般是由于共享的日志文件一直處于打開狀態,因為只能有一個實例去操作,否則內容不好追加。
– 數據庫連接池的設計一般也是采用單例模式,因為數據庫連接是一種數據庫資源。
– 操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統。
– Application 也是單例的典型應用(Servlet編程中會涉及到)
– 在Spring中,每個Bean默認就是單例的,這樣做的優點是Spring容器可以管理
– 在servlet編程中,每個Servlet也是單例
– 在spring MVC框架/struts1框架中,控制器對象也是單例
常見的五種單例模式實現方式
(1)餓漢式
線程安全,調用效率高
package cn.hanquan.pattern; /** 餓漢式單例模式*/ public class PatternTest {// 類加載器初始化的時候,對象立即加載出來// 天然的線程安全,不需要synchronize,調用效率高private static PatternTest instance = new PatternTest();private PatternTest() {// 私有的構造器}public static PatternTest getInstance() { // 唯一公開的方法return instance;} }調用:PatternTest s = PatternTest.getInstance()
package cn.hanquan.pattern; /** 單例對象的使用*/ public class Client {public static void main(String[] args) {PatternTest s1 = PatternTest.getInstance();PatternTest s2 = PatternTest.getInstance();// 無論創建、調用多少次,都是同一個對象System.out.println(s1);System.out.println(s2);// cn.hanquan.pattern.PatternTest@2c13da15// cn.hanquan.pattern.PatternTest@2c13da15} }(2)懶漢式:延遲加載
只有在真正需要用到的時候,才會去加載它,資源利用效率高。缺點是需要synchronize,調用效率低
package cn.hanquan.pattern; /** 懶漢單例模式*/ public class PatternTest {// 類加載器初始化的時候,對象立即加載出來// 天然的線程安全,不需要synchronize,調用效率高private static PatternTest instance;private PatternTest() {// 私有化構造器}public static PatternTest getInstance() {if (instance == null) {instance = new PatternTest();}return instance;} }(3)雙重檢測鎖實現單例模式
由于編譯器優化原因和JVM底層內部模型原因,偶爾會出問題。不建議使用。實際工作用不到。
package cn.hanquan.pattern; /** 雙重檢測鎖實現*/ public class PatternTest {private static PatternTest instance = null; private PatternTest() {}public static PatternTest getInstance() {if (instance == null) {PatternTest sc;synchronized (PatternTest.class) {sc = instance;if (sc == null) {synchronized (PatternTest.class) {if (sc == null) {sc = new PatternTest();}}instance = sc;}}}return instance;} }(4)靜態內部類實現方式
也是一種懶加載方式
– 外部類沒有static屬性,則不會像餓漢式那樣立即加載對象。
– 只有真正調用getInstance(),才會加載靜態內部類。加載類時是線程 安全的。 instance是static final
類型,保證了內存中只有這樣一個實例存在,而且只能被賦值一次,從而保證了線程安全性.
– 兼備了并發高效調用和延遲加載的優勢!
(5)使用枚舉實現單例模式
枚舉是天然的單例模式
? 優點:
– 實現簡單
– 枚舉本身就是單例模式。由JVM從根本上提供保障!避免通過反射和反序列化的漏洞!
? 缺點:
– 無延遲加載
防止反射漏洞
(反射、反序列化對枚舉是無效的)
可以使用反射,直接調用私有的構造器,破解單例模式
- 被破解的單例模式PatternTest.java
- Client2.java利用反射,破解單例模式(注意一下18行的constructor.setAccessible(true),用于取消private檢查)
可以看到,20行和21行,直接調用私有的構造器,獲得不同的對象。
package cn.hanquan.pattern;import java.lang.reflect.Constructor;public class Client2 {public static void main(String[] args) throws Exception {PatternTest s1 = PatternTest.getInstance();PatternTest s2 = PatternTest.getInstance();// 無論創建、調用多少次,都是同一個對象System.out.println(s1);// cn.hanquan.pattern.PatternTest@2c13da15System.out.println(s2);// cn.hanquan.pattern.PatternTest@2c13da15// 利用反射破解單例模式Class<PatternTest> clazz = (Class<PatternTest>) Class.forName("cn.hanquan.pattern.PatternTest");Constructor<PatternTest> constructor = clazz.getDeclaredConstructor(null);// 獲得構造器constructor.setAccessible(true);// 取消private檢查PatternTest s3 = constructor.newInstance();PatternTest s4 = constructor.newInstance();// 直接調用私有構造器,獲得不同的對象System.out.println(s3);// cn.hanquan.pattern.PatternTest@77556fdSystem.out.println(s4);// cn.hanquan.pattern.PatternTest@368239c8} }那么,如何避免別人通過反射破解單例呢?
- PatternTest.java增強版:多次調用時,手動拋出異常
再次運行Client2.java,拋出如下異常:
這樣,成功阻止了:通過反射破解單例模式。
一般情況下,在開發時,不需要考慮這么多。因為我們寫的是項目,而不是JDK…
防止反序列化漏洞
(反射、反序列化對枚舉是無效的)
通過反序列化的方式,構造多個對象
注意,被序列化的對象需要添加implements Serializable
- 被破解的單例模式:PatternTest.java
- Client2.java使用反序列化破解單例模式,創建新的對象
那么,使用單例模式時,如何避免反序列化破解?
- PatternTest.java避免反序列化破解
反序列化會自動調用readResolve()方法。
反序列化時,如果定義了readResolve()方法,則直接返回此方法指定的對象。而不需要單獨創建新對象!
package cn.hanquan.pattern;import java.io.ObjectStreamException; import java.io.Serializable;/** 餓漢式單例模式*/ public class PatternTest implements Serializable {private static PatternTest instance = new PatternTest();private PatternTest() {// 私有的構造器if (instance != null) {throw new RuntimeException();}}public static PatternTest getInstance() { // 唯一公開的方法return instance;}// 反序列化時,如果定義了readResolve()方法,則直接返回此方法指定的對象。而不需要單獨創建新對象!private Object readResolve() throws ObjectStreamException {return instance;} }這樣,Client.java的輸出為三個相同的對象:
cn.hanquan.pattern.PatternTest@2c13da15 cn.hanquan.pattern.PatternTest@2c13da15 cn.hanquan.pattern.PatternTest@2c13da15說明成功避免了利用反序列化破解。
五種單例模式的效率測試
餓漢式:22ms
懶漢式:636ms
靜態內部類式:28ms
枚舉式:32ms
雙重檢查鎖式:65ms
使用CountDownLatch等待其他線程
CountDownLatch是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。
? countDown() 當前線程調此方法,則計數減一(建議放在 finally里執行)
? await(), 調用此方法會一直阻塞當前線程,直到計時器的值為0
總結
以上是生活随笔為你收集整理的【Java设计模式】GOF32 - 单例模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【PAT甲级 补全前导0 vector作
- 下一篇: 【Java设计模式】工厂模式