您好,登錄后才能下訂單哦!
本篇內容主要講解“分析Java中的單例模式”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“分析Java中的單例模式”吧!
維基百科給出了解釋、實現的思路以及應該注意的地方:
單例模式,也叫單子模式,是一種常用的軟件設計模式,屬于創建型模式的一種。在應用這個模式時,單例對象的類必須保證只有一個實例存在。
實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱);當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例并將實例的引用賦予該類保持的引用;同時我們還將該類的構造函數定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。
單例模式在多線程的應用場合下必須小心使用。如果當唯一實例尚未創建時,有兩個線程同時調用創建方法,那么它們同時沒有檢測到唯一實例的存在,從而同時各自創建了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例唯一的原則。解決這個問題的辦法是為指示類是否已經實例化的變量提供一個互斥鎖(雖然這樣會降低效率)。
類圖是:
正如定義所說,單例模式就是整個內存模型中,只有一個實例。實例少了,內存占用就少。同時,只有一個實例,也就只需要構建一個對象,計算就少。對于構造過程中需要大量計算或者占用大量資源的對象,只創建一次,就減少了資源占用和內存占用。
餓漢式是最簡單的一種實現,在類裝載過程中,完成實例化,避免多線程問題。
public class EagerSingleton { private static final EagerSingleton INSTANCE = new EagerSingleton(); private EagerSingleton() { } public static EagerSingleton getInstance() { return INSTANCE; } }
根據java的特性,餓漢式還可以變種寫法,有的地方稱為靜態代碼塊方式:
public class EagerSingleton { private static EagerSingleton INSTANCE = null; static { INSTANCE = new EagerSingleton(); } private EagerSingleton() { } public static EagerSingleton getInstance() { return INSTANCE; } }
這兩種方式只是在寫法上的區別,優缺點沒有區別,只是借助Java語言特性的不同寫法,所以歸為一類。
餓漢式有兩個明顯的缺點:
類裝載過程即完成實例化,如果整個應用生命周期內,實例沒有使用,也就是浪費資源了。
因為沒有辦法向構造函數傳遞不同的參數,如果需要通過個性化參數定制實例時,這種方式就不支持了。
針對餓漢式第一個缺點,我們可以借助靜態內部類的方式,將對象實例化的時間延后。
public class EagerSingleton { private EagerSingleton() { } private static class EagerSingletonInstance { private static final EagerSingleton INSTANCE = new EagerSingleton(); } public static EagerSingleton getInstance() { return EagerSingletonInstance.INSTANCE; } }
但是,依然不能很好的解決第二個缺點,如果需要根據不同的參數實現不同的實例,可以采用下面說的懶漢式實現。
懶漢式比餓漢式的一個優點,就是能夠在使用的時候再進行實例化。但是,餡餅總是要伴隨著陷阱,懶漢式寫法有更多的坑,一不小心就摔著了。
public class LazySingleton { private static LazySingleton INSTANCE = null; private LazySingleton() { } public static LazySingleton getInstance() { if (INSTANCE == null) { INSTANCE = new LazySingleton(); } return INSTANCE; } }
之所以定義為單線程實現,是因為 INSTANCE==null這個判斷,一個線程通過這個判斷,開始進行對象實例化,但是還沒有實例化完成,另一個線程又來了,這個時候,對象還沒有實例化,就也會開始進行實例化,造成不必要的浪費。
public class LazySingleton { private static LazySingleton INSTANCE = null; private LazySingleton() { } public static synchronized LazySingleton getInstance() { if (INSTANCE == null) { INSTANCE = new LazySingleton(); } return INSTANCE; } }
這種方式解決了多線程的問題,但是也引入了新的性能問題:太慢。synchronized把整個方法包起來,也就是每個線程進入的時候,都需要等待其他線程結束調用,才能拿到實例,在性能敏感的場景,是比較致命的。
public class LazySingleton { private static LazySingleton INSTANCE = null; private LazySingleton() { } public static LazySingleton getInstance() { if (INSTANCE == null) { synchronized (LazySingleton.class) { INSTANCE = new LazySingleton(); } } return INSTANCE; } }
這種寫法看似將同步代碼縮小,但也縮小了多線程保障,也犯了第一種寫法的錯誤,屬于沒有對多線程有基本了解寫出的低級錯誤代碼。
public class LazySingleton { private static LazySingleton INSTANCE = null; private LazySingleton() { } public static LazySingleton getInstance() { if (INSTANCE == null) { synchronized (LazySingleton.class) { if (INSTANCE == null) { INSTANCE = new LazySingleton(); } } } return INSTANCE; } }
這種寫法在一定程度上屬于正確的寫法,雙重判斷可以很好的實現線程安全和延遲加載。如果到這里就結束,那就是謬以千里的毫厘之差。
雙重檢查和同步代碼塊都沒有問題,問題出在 INSTANCE=newLazySingleton()這句話。在JVM中,為了充分利用CPU計算能力,會進行重排序優化, INSTANCE=newLazySingleton()做了三件事:
為 INSTANCE 初始化棧空間
為 LazySingleton 分配內存空間,實例化對象
INSTANCE 指向 LazySingleton 實例分配的內存空間
因為重排序優化的存在,真正執行的過程中,可能會出現1-2-3的順序,也可能出現1-3-2的順序。如果是1-3-2,INSTANCE 指向了 LazySingleton 實例分配的內存空間后,就不是null,另外一個線程進入判斷null時,就會直接返回 INSTANCE,但是這個時候 LazySingleton 實例化還沒有完成,就可能出現意想不到的異常。
public class LazySingleton { private static volatile LazySingleton INSTANCE = null; private LazySingleton() { } public static LazySingleton getInstance() { if (INSTANCE == null) { synchronized (LazySingleton.class) { if (INSTANCE == null) { INSTANCE = new LazySingleton(); } } } return INSTANCE; } }
這種寫法比上面那種,就差在 volatile這個關鍵字。
懶漢式和餓漢式都能夠適用于多線程并發場景,但是通過反序列化或反射可以實例化對象,這樣依然不能滿足單例模式的要求,所以可以借助枚舉實現,枚舉可以完美避免多線程并發問題,而且可以防止反序列化和反射創建新對象。第一次看到這樣定義單例模式,是在《Effective Java》中,多讀經典書還是挺好的。
public enum EnumSingleton { INSTANCE; public void method1() { // do something } public Object method2() { // do something and return something else return new Object(); } }
在開發實踐中,枚舉可以滿足絕大部分場景,而且寫法簡單,定義單例的邏輯只需要三行代碼,簡潔而不簡單,三行代碼可以保證線程安全。同時枚舉的反序列化只是通過name查找對象,不會產生新的對象;根據JVM規范,通過反射創建枚舉對象時,會拋出 IllegalArgumentException異常。這樣,相當于通過語法糖防止反序列化和反射破壞單例。
無狀態工具類:這種工具類不需要記錄狀態,只保證正確的應用就行,可以通過單例模式來定義。
數據共享:即多個不相關的兩個線程或者進程之間實現通信。因為是一個實例,如果它的屬性或者變量值被修改,所有引用都是同時修改的,當然需要 volatile 來定義變量。比如網站的計數器。
日志應用:通常應用會向日志文件寫日志信息,為了實時向文件寫,通常會使用單例模式,保證有一個實例持有文件,然后進行操作。
數據庫連接池:數據庫連接是一種數據庫資源,使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,通過單例模式來維護,就可以大大降低這種損耗。
Web應用的配置對象:讀取文件需要消耗時間,如果讀取大文件,消耗的時間和資源更久,所以通過單例模式可以大大降低消耗。
。。。
單例模式的場景還是比較多的,這里只是列出里幾個簡單的應用場景,算是拋磚引玉,如果看官們有什么其他應用場景,可以在說一說
到此,相信大家對“分析Java中的單例模式”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。