什么是單例模式?
單例模式:是指在內存中有且只會創建一次對象的創建型-設計模式,在程序多次使用同一個對象作用相同的時候,為了防止頻繁創建和消費對象,單例模式可以讓程序中只創建一個對象
單例模式的優點:
在內存中只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀對象,避免對資源的占用和浪費
單例模式的缺點:
1、由于單例模式中沒有抽象層,因此單例類的擴展有很大的困難
2、單例類職責過重,在一定程度違背了 “單一職責原則”
3、濫用單例將帶來一些負面問題,如為了節省資源將數據庫連接池設計為單例類,會導致共享連接池對象過多出現連接池溢出,如果實例出的對象長時間不被利用,系統會認為是垃圾進行回收,這將導致對象的狀態丟失
單例模式的適用場景:
1、需要頻繁實例化然后銷毀的對象
2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象
3、一些開發工具類
4、一些大量使用的配置文件對象
單例模式的特點:
1、單例模式的構造方法是私有的
2、單例對象必須由單例類自行創建
3、單例類對外提供統一獲取實例的靜態方法
單例模式的實現;
1、餓漢式單例(線程安全)
餓漢式是最簡單的單例模式寫法,類創建的同時就已經創建好一個靜態的對象供系統使用,以后不再改變,所以保證了線程安全,但是如果長時間沒使用這個方法,會浪費系統資源,所以不建議使用。
/**
* @Author LvHui
* @Date 13:28 2022/7/14
* @Description 單例模式的實現之餓漢式
* 優點:線程安全,調用效率高,
* 缺點:不能延遲加載,如果不使用則會浪費系統資源
* 測試后結論;因為是類初始化的時候加載的靜態變量,所屬在使用的時候不會出現線程安全的問題,又因為獲取實例方法沒有同步鎖所以效率會高
**/
public class HungerType {
//類初始化時,立即加載這個對象
private static final HungerType hungerType = new HungerType();
//私有化構造器
private HungerType() {
}
//提供統一的訪問方法
public static HungerType getInstance() {
return hungerType;
}
}
2、懶漢式單例(線程不安全)
該模式的特點是類加載時沒有生成單例,只有當第一次調用 getlnstance 方法時才去創建這個單例。代碼如下:
/**
懶漢式單例-線程不安全
**/
public class LazyType {
private static LazyType lazyType = null;
private LazyType() {
}
public static LazyType getInstance() {
if (lazyType == null) {
lazyType = new LazyType();
}
return lazyType;
}
}
這種方式實現的單例模式有一個問題,如果多個線程同時調用getInstance()方法時,由于沒有鎖機制,會導致實例化兩個實例的情況,因此在多線程環境下是不安全
3、懶漢式單例(線程安全)
/**
懶漢式單例-線程安全
**/
public class LazyType {
private static LazyType lazyType = null;
private LazyType() {
}
//通過增加同步鎖來解決線程安全問題
public synchronized static LazyType getInstance() {
if (lazyType == null) {
lazyType = new LazyType();
}
return lazyType;
}
}
如上代碼所示,getInstance()方法添加了同步鎖,雖然解決了線程安全問題,但卻也帶來了另外一個問題,就是每次獲取實例都需要加鎖和釋放鎖,效率較低,繼續往下優化代碼
4、懶漢式單例(雙重檢測DCL)
經過思考我們判斷只有第一次獲取實例的時候才會有線程安全問題,所以我們考慮使用懶加載方式,只需要第一次的時候進行加鎖,后續的操作直接返回實例,但如果只有一層判斷后再加鎖會有問題,如第一次兩個線程同時滿足第一個判空條件,然后等待獲取鎖,破壞了單例唯一性條件,所以需要進行二次校驗判空語句。
/**
懶漢式單例-雙重檢測(DCL即 double-checked locking)
**/
public class LazyType {
//增加volatile關鍵字
private static volatile LazyType lazyType = null;
private LazyType() {
}
//采用雙層檢測方式獲取實例對象
public static LazyType getInstance() {
if (lazyType == null) {
synchronized (LazyType .class){
if (lazyType == null) {
lazyType = new LazyType();
}
}
}
return lazyType;
}
}
但這樣就沒問題了嗎?
由于java內存模型允許 “無序寫入”,有些時候編譯器會因為性能問題,把代碼進行指令重排序,可能順序發生顛倒一般而言初始化操作并不是一個原子操作,而是分為三步
1、在堆內存中開辟對象所需要的空間,分配地址
2、根據類加載器的順序進行初始化對象執行構造方法
3、把堆分配的地址返回給棧中的引用變量
在成員變量lazyType上修飾了volatile關鍵字,該關鍵字是為了保證可見性,防止指令重排帶來的順序性問題,因為初始化對象。不是一個原子操作,jvm可能為了優化代碼會進行指令重排,導致執行了順序發生改變,如執行流程原本是1->2->3,但可能由于指令重排改為1->3->2,這就會導致如果執行到第二步操作,另一個線程獲取實例,判斷不為空,獲取到尚未構造完成的對象,所以為了避免這個問題,則需要用到volatile關鍵字來防止指令重排保證操作的有序性。
5、靜態內部類單例(線程安全,懶加載)
靜態內部類單例模式也比較推薦的一種單例實現,因為相比懶漢式,它用更少的代碼量達到了延遲加載的目的,這種方式不僅能夠保證線程安全,也能保證單例對象的唯一性,同時也延遲實例化,是一種非常推薦的方式。
那么靜態內部類是如何實現線程安全呢?首先,我們先了解一下類的加載時機。
java虛擬機在僅有5種情況下會對類進行初始化
1、遇到new、getstatic、setstatic或者invokestatic這4個字節碼指令時,對應的java代碼場景為:new一個關鍵字或者一個實例化對象時讀取、或設置一個靜態字段時(final修飾、已在編譯期把結果放入常量池的除外)、調用一個類的靜態方法時。
2、使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒進行初始化,需要先調用其初始化方法進行初始化
3、初始化一個類的時候發現有父類還未進行初始化,則先初始化父類
4、當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
5.當使用JDK 1.7等動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
這5種情況被稱為是類的主動引用,注意,這里《虛擬機規范》中使用的限定詞是"有且僅有",那么,除此之外的所有引用類都不會對類進行初始化,稱為被動引用。靜態內部類就屬于被動引用的行列。
當getInstance()方法被調用時,靜態內部類才在StatisticsInnerClassType 的運行時常量池里,把符號引用替換為直接引用,這時靜態對象INSTANCE也真正被創建,這點同餓漢模式。那么INSTANCE在創建過程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機》中,有這么一句話:
虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個進程阻塞(需要注意的是,其他線程雖然會被阻塞,但如果執行<clinit>()方法后,其他線程喚醒之后不會再次進入<clinit>()方法。同一個加載器下,一個類型只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。
故而,可以看出INSTANCE在創建過程中是線程安全的,所以說靜態內部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。
/**
* @Author LvHui
* @Date 23:37 2022/7/16
* @Description 單例模式-靜態內部類方式 懶加載
* 靜態內部類能實現單例模式有以下兩點
* 1、jvm在加載類的時候不會加載靜態內部類,在使用到靜態內部類的時候才會對靜態內部類進行加載,和懶漢模式一樣,節省系統資源
* 2、jvm底層保證類加載的安全,即使在高并發情況下,類的加載只有一次,就保證了創建單例時并發安全性
**/
public class StatisticsInnerClassType {
private StatisticsInnerClassType() {
}
public static StatisticsInnerClassType getInstance() {
return InnerClass.HOLDER;
}
/**
* 靜態內部類與外部類的實例沒有綁定關系,而且只有被調用時才會
* 加載,從而實現了延遲加載
*/
public static class InnerClass {
/**
* 靜態初始化器,由JVM來保證線程安全
*/
private static final StatisticsInnerClassType HOLDER = new StatisticsInnerClassType();
}
}
6、枚舉單例(線程安全,天生防止反射)
以上靜態內部類的方式雖然解決了線程安全和延遲加載的問題,但是,還是存在一些如下問題
1、反射可以獲取到構造函數并設置為可訪問,并生成新的對象。
2、clone的深克隆會生成新的對象
3、反序列化生成新的對象
由于單例模式的枚舉實現代碼比較簡單,而且又可以利用枚舉的特性來解決線程安全和單一實例的問題,還可以防止反射和反序列化對單例的破壞,通過實現Serializable接口增加readResolve方法來防止反序列化對單例的破壞,具體參考以下代碼
/**
* @Author LvHui
* @Date 13:28 2022/7/14
* @Description 單例模式的實現之枚舉
* 由于單例模式的枚舉實現代碼比較簡單,而且又可以利用枚舉的特性來解決線程安全和單一實例的問題,還可以防止反射和反序列化對單例的破壞
* 通過實現Serializable接口增加readResolve方法來防止反序列化對單例的破壞
**/
public class EnumSingle implements Serializable {
private static final long serialVersionUID = 1L;
private EnumSingle() {
}
public enum SingletonEnum {
SINGLETON;
private EnumSingle enumSingle = null;
SingletonEnum() {
enumSingle = new EnumSingle();
}
public EnumSingle getInstance() {
return enumSingle;
}
}
//此方法來防止反序列化對單例的破壞,具體可參考ObjectInputStream的readObject()方法
private Object readResolve() {
return SingletonEnum.SINGLETON.getInstance();
}
}
為什么枚舉類型是線程安全的?
通過反編譯枚舉的class文件發現屬性被static final修飾,根據類加載機制,static類型的屬性和方法會在類加載的過程中初始化,當第一次調用初始化類,因為初始化加載類的時候classload方法是加鎖保證線程安全的,所以enum是線程安全的。
為什么枚舉類型反序列化也不會創建新的實例?
枚舉類型在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找values()數組種的枚舉對象同時,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,而普通的類反序列化是通過反射重新創建對象,破壞了單例的唯一原則。所以枚舉類型反序列化也不會創建新的實例
單例的模式每種各有優缺點,但推薦的還是靜態內部類和枚舉的單例模式。
本人水平有限,如果有地方存在一些錯誤和不足,麻煩各位提醒,這邊我會及時修改錯誤的地方避免影響大家的理解。