一、概述
1、什么是單例設計模式?
在某些特殊場合中,一個類只能夠產生一個實例對象,並且這個實例對象要可以對外提供訪問。這樣的類叫做單例類, 而設計單例的流程和思想叫做單例設計模式。
單例模式屬於設計模式三大類中的創建型模式。
2、單例設計模式的特點
單例模式具有典型的三個特點:
- 只有一個實例。
- 自我實例化。
- 提供全局訪問點。
注意:
注:注意單例模式所屬類的
構造方法是私有的,所以單例類是
不能被繼承的。
(這句話表述的有點問題,單例類一般情況只想內部保留一個實例對象,所以會選擇將構造函數聲明為私有的,這才使得單例類無法被繼承。單例類與繼承沒有強關聯關系。)
3、單例設計模式的UML類圖
單例模式的UML結構圖非常簡單,就只有一個類,如下圖:
Singleton類,定義一個靜態方法,getInstance(),可以通過類名來調用,主要負責替代構造方法,創建Singleton類唯一的實例對象。
這個類可以對外提供訪問,允許用戶通過getInstance()方法訪問它唯一的實例。
4、單例設計模式的優缺點:
優點:
1)、由於單例模式只生成了一個實例,所以能夠節約系統資源,減少性能開銷,提高系統效率,
2)、避免頻繁的創建銷毀對象,可以提高性能;
3)、避免對共享資源的多重占用,簡化訪問;
4)、為整個系統提供唯一一個全局訪問點,能夠嚴格控制客戶對它的訪問。
缺點:
1)、不適用於變化頻繁的對象;
2)、也正是因為系統中只有一個實例,這樣就導致了單例類的職責過重,違背了“單一職責原則”,
濫用單例將帶來一些負面問題,如為了節省資源將數據庫連接池對象設計為的單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出;
3)、同時也沒有抽象類,這樣擴展起來有一定的困難。
4)、如果實例化的對象長時間不被利用,系統會認為該對象是垃圾而被回收,這可能會導致對象狀態的丟失;(這個所有的對象都會,跟單例無關。)
5、單例模式的應用場景:
場景一:
windows的任務管理器,無論你點擊多少次,始終都只有一個管理器窗口存在,系統並不會為你創建新的窗口,也就是說,整個系統運行的過程中,系統只維護了一個進程管理器的實例。這就是一個典型的單例模式運用。
場景二:
線程池、數據庫連接池的設計一般也是采用單例模式,因為數據庫連接是一種數據庫資源。數據庫軟件系統中使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,用單例模式來維護,就可以大大降低這種損耗。
場景三:
程序的日志模塊。一般也是采用單例模式實現。由於共享的日志文件一直處於打開狀態,只能有一個實例去操作,否則內容不好追加。 采用單例模式就可以。
場景四:
Web應用的配置對象的讀取,一般也應用單例模式,這個是由於配置文件是共享的資源。
這些配置信息存放在一個文件中,由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復雜環境下的配置管理。
場景五:
在我們的實際項目開發中,可以使用單例模式來封裝一些常用的工具類,保證整個應用常用的數據統一。或者保存一些共享數據在內存中,其他類隨時可以讀取。
二、單例模式的實現步驟
可以使用如下的步驟實現一個單例類:
單例設計模式的實現流程
1、將構造方法私有化,使用private關鍵字修飾。使其不能在類的外部通過new關鍵字實例化該類對象。 2、在該類內部產生一個唯一的實例化對象,並且將其封裝為private static類型。 3、對外提供一個靜態方法getInstance()負責將對象返回出去,使用public static修飾
三、單例模式的實現方式 (推薦枚舉類方式)
1、餓漢式——立即加載
線程安全,調用效率高。但是不能延時加載。
立即加載就是加載類的時候就已經將對象創建完畢(不管以后會不會使用到該實例化對象,先創建了再說。很着急的樣子,故又被稱為“餓漢模式”),常見的實現辦法就是直接new實例化。
所以加載類的速度比較慢,但是獲取對象的速度比較快,且是線程安全的。
/**
* 餓漢式
*/
public class Singleton {
// 創建全局唯一的實例化對象,在類初始化時,就會立即加載這個對象
private static Singleton instance = new Singleton();
// 私有化構造方法
private Singleton() {}
// 提供公有靜態方法返回對象
public static Singleton getInstance() {
return instance;
}
}
我們知道,類加載的方式是按需加載,且加載一次。因此,在上述單例類被加載時,就會實例化一個對象並交給自己的引用,供系統使用;而且,由於這個類在整個生命周期中只會被加載一次,因此只會創建一個實例,即能夠充分保證單例。
優缺點:
優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。(因為這個static的instance對象會一直占着這段內存,直到卸載類(即便你還沒有用到這個實例))
2、懶漢式——延遲加載
延遲加載就是調用get()方法時實例才被創建(先不急着實例化出對象,等要用的時候才給你創建出來。不着急,故又稱為“懶漢模式”),常見的實現方法就是在get方法中進行new實例化。
/**
* 懶漢式
*/
public class Singleton {
// 聲明一個自身實例對象的引用
private static Singleton instance;
// 私有化構造方法
private Singleton(){}
// 提供公有靜態方法返回對象
public static Singleton getInstance() {
// 判斷如果為空,就創建,如果已經有了,就直接返回該實例,避免重復創建,保證全局唯一
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
由於該模式是在運行時加載對象的,所以加載類比較快,但是對象的獲取速度相對較慢,且線程不安全。如果想要線程安全的話可以加上synchronized關鍵字,但是這樣會付出慘重的效率代價。
我們從懶漢式單例可以看到,單例實例被延遲加載,即只有在真正使用的時候才會實例化一個對象並交給自己的引用。
這種寫法起到了Lazy Loading的效果,但是只能在單線程下使用。如果在多線程下,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。所以在多線程環境下不可使用這種方式。
“懶漢模式”的優缺點:
優點:實現起來比較簡單,當類SingletonTest被加載的時候,靜態變量static的instance未被創建,只是聲明了一個引用,並未分配內存空間。要當getInstance方法第一次被調用時,初始化instance變量,才會真正創建對象,開始分配內存,因此在某些特定條件下會節約了內存。(需要時才創建)
缺點:在多線程環境中,這種實現方法是完全錯誤的,根本不能保證單例的狀態。
3、線程安全的“懶漢模式”—— synchronized
在懶漢模式的基礎上,增加了synchronized鎖同步機制,保證全局唯一。
/**
* 3、線程安全的懶漢式 —— synchronized
*/
public class Singleton {
// 聲明一個自身實例對象的引用
private static Singleton instance;
// 私有化構造方法
private Singleton(){}
// 提供公有靜態方法返回對象,加上synchronized關鍵字實現同步
public static synchronized Singleton getInstance() {
// 判斷如果為空,就創建,如果已經有了,就直接返回該實例,避免重復創建,保證全局唯一
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
優點:在多線程情形下,保證了“懶漢模式”的線程安全。
缺點:眾所周知在多線程情形下,synchronized方法通常效率低,顯然這不是最佳的實現方案。
4、懶漢式(DCL雙重檢測鎖)
DCL雙檢查鎖機制(DCL:double checked locking)
/**
* 4、懶漢式 —— DCL雙重檢查鎖機制(類鎖)
* 再一次縮小了鎖的范圍,提供了性能
*/
public class Singleton {
// 聲明一個自身實例對象的引用,使用volatile保證多線程下引用的一致性
private static volatile Singleton instance;
// 私有化構造方法
private Singleton(){}
// 提供公有靜態方法返回對象
public static Singleton getInstance() {
// 第一次檢查instance是否被實例化出來,如果沒有,再加鎖處理
if (instance == null) {
synchronized (Singleton.class) {
// 某個線程取得了類鎖,實例化對象前第二次檢查instance是否已經被實例化出來,如果沒有,才最終實例出對象
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Double-Check概念對於多線程開發者來說不會陌生。
我們這里相比3直接對靜態方法getInstance加上synchronized鎖的方式,縮小了鎖的范圍。
將第一個if判斷塊釋放出來了,如果實例存在,則根本不會鎖住,大大加快了返回實例的效率。
只有當第一次if檢查后,確定實例是真的不存在,需要創建時,此時才會開始加鎖,注意此時加的是類鎖,不是對象鎖。不過這里是static靜態方法,對象也是靜態的,所以實際上它們效果是一樣的。
為什么在鎖里面還要再次判定是否為空呢?
因為高並發,后面的線程在第一次判定實例時也為空,也可以獲得鎖,只是要排隊,只是在等待前面的線程釋放鎖。所以,當輪到它拿到鎖之后,可能前面的線程已經創建了實例,所以要再次判定是否為空。這樣才能保證實例唯一。
(重點在於,多個線程可以同時通過第一個if,然后都可以按順序執行鎖里的代碼。)
Java指令重排的問題
注意:單純使用上面這種方式,仍然是線程不安全的。
因為存在java指令重排的問題。
在java創建對象的時候,cpu按照以下三個步驟來執行:
1、memory = allocate() 在堆內存中開辟對象的內存空間,並指定地址
2、根據類加載的順序,初始化對象。
3、instance = memory 設置instance指向剛分配的內存地址。instance是變量,存在棧中。
單純執行以上三步沒啥問題,但是在多線程情況下,可能會發生指令重排序。
指令重排序對單線程沒有影響,單線程下CPU可以按照順序執行以上三個步驟,但是在多線程下,如果發生了指令重排序,則會打亂上面的三個步驟。
如果發生了JVM和CPU優化,發生重排序時,可能會按照下面的順序執行:
1、memory = allocate() 在堆內存中開辟對象的內存空間,並指定地址
3、instance = memory 設置instance指向剛分配的內存地址。instance是變量,存在棧中。
2、根據類加載的順序,初始化對象。
假設目前有兩個線程A和B同時執行getInstance()方法,
- A線程執行到instance = new Singleton(); B線程剛執行到第一個 if (instance == null) 處,
- 如果按照1.3.2的順序,假設線程A執行到第三步3.instance = memory 設置instance指向剛分配的內存,此時,線程B判斷instance已經有值,就會直接return instance;
- 而實際上,線程A還未執行第二步 初始化對象,也就是說線程B拿到的instance對象還未進行初始化,這個未初始化的instance對象一旦被線程B使用,就會出現問題。
5、懶漢式(DCL雙重檢測鎖機制+volatile禁止指令重排)—— 推薦
相比4,這里對引用加入了volatile機制,禁止java的指令重排
懶漢式的單例模式的最佳實現方式。內存消耗少,效率高,線程安全,多線程操作原子性。
/**
* 5、懶漢式 —— DCL雙重檢查鎖機制(類鎖) + volatile禁止指令重排
* 再一次縮小了鎖的范圍,提供了性能。(推薦)
*/
public class Singleton {
// 聲明一個自身實例對象的引用,使用volatile禁止指令重排,保證多線程下引用的一致性
private static volatile Singleton instance;
// 私有化構造方法
private Singleton(){}
// 提供公有靜態方法返回對象
public static Singleton getInstance() {
// 第一次檢查instance是否被實例化出來,如果沒有,再加鎖處理
if (instance == null) {
synchronized (Singleton.class) {
// 某個線程取得了類鎖,實例化對象前第二次檢查instance是否已經被實例化出來,如果沒有,才最終實例出對象
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
線程安全;延遲加載;效率較高。
6、靜態代碼塊——立即加載
靜態代碼塊方式跟餓漢式的方式幾乎是一樣的,只是把初始化代碼放到了static塊中了。
因為我們知道,類加載的時候,這些屬性和靜態代碼塊都是會跟隨類一起加載的,所以它的實現方式和餓漢式一樣。也是線程安全的。
/**
* 6、靜態代碼塊方式
* 方式類似餓漢式,也是立即加載,是線程安全的
*/
public class Singleton {
// 在外部聲明一個對象的引用,注意不能放到靜態代碼塊中
private static Singleton instance;
// 靜態代碼塊中,創建唯一實例對象,賦值給引用。
static {
instance = new Singleton();
}
// 私有化構造方法
private Singleton() {}
// 提供公有靜態方法返回實例對象
public static Singleton getInstance() {
return instance;
}
}
優缺點:
優缺點都同餓漢式一樣,也是立即加載,線程安全的。
這里定義靜態變量時要注意:
靜態變量只能定義在類的內部,不可以定義在靜態塊或方法中。可以在類內部定義靜態變量,在靜態塊中進行初始化操作,因為類的內部是不允許有操作語句存在的,比如JDBC操作,所以可以在靜態塊static{} 中進行初始化操作,如:JDBC 定義靜態變量主要是為了供外部訪問,定義在一個局部中外部沒有權限訪問,為什么要定義呢,而且不能定義。
7、靜態內部類
懶漢模式需要考慮線程安全,所以我們多寫了好多的代碼,餓漢模式利用了類加載的特性為我們省去了線程安全的考慮,那么,既能享受類加載確保線程安全帶來的便利,又能延遲加載的方式,就是靜態內部類。Java靜態內部類的特性是,加載的時候不會加載內部靜態類,使用的時候才會進行加載。而使用到的時候類加載又是線程安全的,這就完美的達到了我們的預期效果~
/**
* 7、靜態內部類
* 融合餓漢式和懶漢式的優點,推薦
*/
public class Singleton {
// 私有靜態內部類中創建並初始化實例對象,注意要private私有化,不能被外部調用了
private static class SingletonInner{
private static Singleton instance = new Singleton();
}
// 私有化構造方法
private Singleton() {}
// 提供公有靜態方法,返回實例對象
public static Singleton getInstance() {
return SingletonInner.instance;
}
}
似乎靜態內部類看起來已經是最完美的方法了,其實不是,可能還存在反射攻擊或者反序列化攻擊。
8、枚舉類 —— 線程最安全(最佳方式)
單元素的枚舉類型已經成為實現Singleton的最佳方法
-- 出自 《effective java》
在effective java(這本書真的很棒)中說道,最佳的單例實現模式就是枚舉模式。利用枚舉的特性,讓JVM來幫我們保證線程安全和單一實例的問題。除此之外,寫法還特別簡單。
/**
* 8、枚舉類
* 最佳實現方式
*/
public enum Singleton {
INSTANCE;
}注意:
因為INSTANCE實例是public公有的,可以直接通過類名的方式調用,即Singleton.INSTANCE,
就不再需要提供公有靜態方法getInstance()來返回對象了。
這是最簡潔、最安全的方式,不過它不能實現lazy loading延遲加載。
其實枚舉類它本身就具備單例的特性:
比如:都會私有化構造方法。枚舉類會對屬性值加上public static final的屬性,保障這個屬性值都是全局唯一的。這些操作都和單例很像
所以把這個屬性變成對象,它就是一個單例類。
類似於這種內部類的形式:
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
}
枚舉類繼承自ENUM,內部實現了Serializable接口,所以不用考慮序列化的問題(其實序列化反序列化也能導致單例失敗的,但是我們這里不過多研究)。
對於線程安全,同樣的,加載的時候JVM能確保只加載一個實例。避免暴力反射創建多個實例,絕對防止多次實例化。
枚舉類最佳實踐:
參考:https://www.jianshu.com/p/d35f244f3770
枚舉單例示例:
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
實際應用場景中,很多人會這么使用枚舉單例:
public class User {
//私有化構造函數
private User(){ }
//定義一個靜態枚舉類
static enum SingletonEnum{
//創建一個枚舉對象,該對象天生為單例
INSTANCE;
private User user;
//私有化枚舉的構造函數
private SingletonEnum(){
user=new User();
}
public User getInstnce(){
return user;
}
}
//對外暴露一個獲取User對象的靜態方法
public static User getInstance(){
return SingletonEnum.INSTANCE.getInstnce();
}
}
public class Test {
public static void main(String [] args){
System.out.println(User.getInstance());
System.out.println(User.getInstance());
System.out.println(User.getInstance()==User.getInstance());
}
}
結果為true
以上代碼看起來已經是ok了,其實不是,可能還存在反射攻擊或者反序列化攻擊
最終版
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
// 調用方法:
public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
}
// 直接通過Singleton.INSTANCE.doSomething()的方式調用即可。方便、簡潔又安全。
推薦大家使用枚舉類實現單例模式。
四、各種實現方式的選擇
一般情況下,懶漢式(包含線程安全和線程不安全兩種方式)都比較少用;
餓漢式和DCL雙重檢測鎖都可以使用,可根據具體情況自主選擇;
在要明確實現 lazy loading 效果時,可以考慮靜態內部類的實現方式;
若涉及到反序列化創建對象時,大家也可以嘗試使用枚舉方式。
在選擇時,請參考下面這張圖:
圖片來源:https://www.cnblogs.com/rainbowbridge/p/12902359.html
五、破壞單例模式的方法及解決辦法
參考:https://blog.csdn.net/b_just/article/details/104061314
1、除枚舉方式外, 其他方法都會通過反射的方式破壞單例,反射是通過調用構造方法生成新的對象,所以如果我們想要阻止單例破壞,可以在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法如下:
private SingletonObject1(){
if (instance !=null){
throw new RuntimeException("實例已經存在,請通過 getInstance()方法獲取");
}
}
2、如果單例類實現了序列化接口Serializable, 就可以通過反序列化破壞單例,所以我們可以不實現序列化接口,如果非得實現序列化接口,可以重寫反序列化方法readResolve(), 反序列化時直接返回相關單例對象。
public Object readResolve() throws ObjectStreamException {
return instance;
}
引用轉載:
https://www.jianshu.com/p/3f5eb3e0b050 (爆贊)
https://www.cnblogs.com/xuwendong/p/9633985.html (爆贊)
https://segmentfault.com/a/1190000010755849 (贊)
https://www.cnblogs.com/binaway/p/8889184.html


