單例模式(Singleton Pattern):採取一定的方法保證在整個的軟體系統中,對某個類只能存在一個物件例項,並且該類只提供一個取得其物件例項的方法。
通俗點來講:就是一個男人只能有一個老婆,一個女人只能有一個老公
單例模式一共有8種方式實現,下面一一舉例:
1、餓漢式(靜態常量屬性)
實現步驟
構造器私有化
- 在類的內部定義
靜態常量物件
- 向外暴露一個
靜態的公共方法getInstance,返回一個類的物件例項
參考程式碼
/**
* Husband,一個女人只能有一個老公husband -> Husband實現單例模式
* 方式1:餓漢式(靜態常量屬性)實現單例模式
*/
public class Husband {
// 第二步:建立私有的靜態常量物件
private static final Husband husband = new Husband();
// 第一步:構造器私有化
private Husband() {
}
// 第三步:向外提供一個靜態的公共方法,以獲取該類的物件
public static Husband getInstance() {
// 返回的永遠是 同一個 husband物件
return husband;
}
}
細節說明
- 為什麼構造器要私有化? -> 使外部無法建立Husband物件,只能使用已經準備好的物件 -> 不能濫情
- 為什麼屬性設定成final? -> 只建立這一個物件husband,以後不會再變 -> 一生只鍾情一人
- 為什麼設定成類變數static? -> 構造器已被私有化,外部無法建立Husband物件,需要類內部提前準備好物件 -> 在類載入時將物件準備好
- 為什麼公共方法要用static? -> 非static方法屬於物件,需要透過物件.方法名()呼叫 -> 外部類無法建立物件,在沒有物件時,外部類無法訪問非static方法
- 為什麼叫餓漢式? -> 只要載入了類資訊,物件就已經建立好 -> 只要餓了,就吃東西
優缺點
- 優點:實現了在整個軟體過程中建立一次類的物件,不存線上程安全問題(反射會破壞單例模式安全性),呼叫效率高
- 缺點:如果只載入了類,並不需要用到該類物件,物件也已經建立好 -> 存在記憶體資源浪費問題。
2、餓漢式(靜態程式碼塊)
實現步驟
- 構造器私有化
- 定義靜態常量不初始化
- 靜態程式碼塊中為類屬性初始化建立物件
- 向外提供一個靜態的公共方法,以獲取該類的物件
參考程式碼
/**
* Husband,一個女人只能有一個老公 -> Husband實現單例模式
* 方式2:餓漢式(靜態程式碼塊)實現單例模式
*/
public class Husband {
// 第二步:建立私有的靜態常量物件
private static final Husband husband;
static {
husband = new Husband();
}
// 第一步:構造器私有化 -> 外部無法建立該類物件,只能使用已經準備好的物件
private Husband() {
}
// 第三步:向外提供一個靜態的公共方法,以獲取該類的物件
public static Husband getInstance() {
// 返回的永遠是 同一個 husband物件
return husband;
}
}
細節說明:原理和第一種餓漢式相同,都是利用類載入時完成物件屬性的建立
優缺點同上一種餓漢式
3、懶漢式(執行緒不安全)
實現步驟
- 構造器私有化
- 定義一個私有靜態屬性物件
- 提供一個公共的static方法,可以返回一個類的物件
參考程式碼
/**
* 一個男人只能有一個老婆wife -> Wife類實現單例模式
* 方式3:懶漢式單例模式(執行緒不安全)
*/
public class Wife {
// 第二步:設定私有靜態屬性
private static Wife wife = null;
// 第一步:構造器私有化
private Wife() {
}
// 第三步:向外提供獲取物件的靜態方法
public static Wife getInstance() {
if (wife == null) { // 如果沒有老婆,則分配一個老婆
wife = new Wife();
}
return wife;
}
}
細節說明
- 為什麼叫懶漢式? -> 即使載入了類資訊,不呼叫getInstance()方法也不會建立物件 -> 餓了(載入類資訊)也不吃東西(不建立物件),懶到一定程度。
- 為什麼要if判斷? -> 防止每次呼叫時都會重新建立新的物件 -> 防止濫情
優缺點
-
優點:只有需要物件時才會呼叫方法返回該類物件,沒有則建立物件,避免了資源浪費 -> 實現了延時載入
-
缺點:執行緒不安全,分析if程式碼塊:
if (wife == null) { // 當執行緒1進入if程式碼塊後,還沒有完成物件的建立之前,執行緒2緊隨其後也進入了if程式碼塊內 // 此時就會出現執行緒1建立了物件,執行緒2也建立了物件 -> 破壞了單例模式 -> 執行緒不安全 wife = new Wife(); // 通俗的來講:多個女生看上了同一個男生,問男生有沒有老婆?男生回答沒有老婆 -> 多個女生先後都當過男生的老婆 -> 前妻太多 -> 違背了單例模式的"一生只鍾情一人"的核心思想 }
4、懶漢式(同步程式碼塊)
參考程式碼
/**
* 一個男人只能有一個老婆wife -> Wife類實現單例模式
* 方式4:懶漢式單例模式(同步程式碼塊,執行緒安全,效能差)
*/
public class Wife {
// 第二步:設定私有靜態屬性
private static Wife wife = null;
// 第一步:構造器私有化
private Wife() {
}
// 第三步:向外提供獲取物件的靜態方法
public static Wife getInstance() {
synchronized(Wife.class) { // 同步程式碼塊,每個執行緒進入if判斷前都需要獲得互斥鎖,保證同一時間只有一個執行緒進入
if (wife == null) {
wife = new Wife();
}
}
return wife;
}
}
說明:給if語句加上synchronized關鍵字,保證每一次只有一個執行緒獲得互斥鎖進入同步程式碼塊,並且將同步程式碼塊全部執行完之後釋放鎖,切換其他執行緒執行,類似於資料庫中事務的概念(給SQL語句增加原子性),這裡是給if語句增加原子性,要麼全部執行,要麼都不執行。
優缺點
-
優點:執行緒安全(反射會破壞安全性)
-
缺點:效能差 -> 只有第一次建立物件時需要同步程式碼,確保同一時間只有一個執行緒進入if語句,後面執行緒再呼叫該方法時,物件已經建立好只需要直接返回 -> 每一次執行緒呼叫該方法後,都需要等待獲取其他執行緒釋放的互斥鎖 -> 浪費了大量時間在 等待獲取互斥鎖 上 -> 效率低下
通俗的來講:多個女生問同一個男生有沒有老婆? -> 男生回答:需要成為物件才有資格知道(設定同步程式碼塊,執行緒需要獲取互斥鎖才能執行程式碼) -> 每一個女生都需要經過一段長時間的發展,處成物件(執行緒獲取互斥鎖) -> 男生告訴自己的物件自己沒有老婆(一個執行緒進入if判斷) -> 男生有了老婆(建立物件) -> 返回物件
5、懶漢式(同步方法)
參考程式碼
/**
* 一個男人只能有一個老婆wife -> Wife類實現單例模式
* 方式5:懶漢式單例模式(同步方法,執行緒安全,效能差)
*/
public class Wife {
// 第二步:設定私有靜態屬性
private static Wife wife = null;
// 第一步:構造器私有化
private Wife() {
}
// 第三步:向外提供獲取物件的靜態方法
public synchronized static Wife getInstance() { // 同步方法,原理和同步程式碼塊實現懶漢式相同
if (wife == null) {
wife = new Wife();
}
return wife;
}
}
優缺點:和同步程式碼塊實現懶漢式類似,這裡不過多贅述。
6、懶漢式(DCL模式⭐)
DCL模式實現懶漢式單例模式,即雙重檢查機制(DCL, Double Check Lock),執行緒安全,效能高 <- 面試重點
參考程式碼
/**
* 一個男人只能有一個老婆wife -> Wife類實現單例模式
* 方式6:懶漢式單例模式(DCL模式 -> 雙重檢查,執行緒安全,效能高)
*/
public class Wife {
// 第二步:設定私有靜態屬性
// volatile關鍵字:極大程度上避免JVM底層出現指令重排情況,極端情況除外
private static volatile Wife wife = null;
// 第一步:構造器私有化
private Wife() {
}
// 第三步:向外提供獲取物件的靜態方法
public static Wife getInstance() {
// 第一層if判斷作用:當物件已經建立好時,直接跳過if語句,返回已經建立好的物件,不在等待獲取互斥鎖 -> 節省時間,提高效能
if (wife == null) {
// 注意:這裡容易有多個執行緒同時進入第一層if的程式碼塊中,等待獲取物件鎖
synchronized (Wife.class) { // 同步程式碼塊,保證每個執行緒進入if判斷前都需要獲得互斥鎖
// 第二層if判斷作用:當有多個執行緒都進入了第一層if語句內,會出現執行緒1進入時物件為空,則建立物件,釋放互斥鎖,執行緒2獲得互斥鎖後如果沒有第二層if判斷,則直接建立物件,破壞了單例模式 -> 第二層if保證執行緒安全
if (wife == null) {
wife = new Wife();
// 在JVM底層建立物件時,大致分為3條指令
// 1.分配記憶體空間 -> 2.構造器初始化 -> 3.物件引用指向記憶體空間
// JVM為了執行效率,會打亂指令順序(指令重排),有可能是1 -> 3 -> 2
// 當執行到3時,物件還沒有建立完成,但是其他執行緒在第一層if判斷已經建立好物件直接返回,顯然不合理(物件屬性還沒有初始化完成) -> 保證指令執行順序不被打亂(保證單條語句編譯後的原子性) -> 使用volatile變數,禁止JVM最佳化重排指令
}
}
}
return wife;
}
}
DCL模式的兩層if判斷的作用:
- 第一層if:已經建立好物件時直接返回,不再排隊獲取互斥鎖,提升效率
- 第二層if:保證執行緒安全
通俗的來講:
-
有多個女生問同一個男生有沒有老婆?
-
-> 男生口頭回答說沒有老婆(進入第一層if判斷,如果有老婆則直接遠離:不要去碰一個已婚的男人,他是一個女人的餘生,不是你的男人不要情意綿綿) -> 其中一個女生和男生處成物件(一個執行緒獲取到互斥鎖) -> 經過發展後,女生和男生登記結婚,民政局辦理結婚證時檢查男生婚姻情況(第二層if判斷) --未婚--> 成為夫妻,男生獲得老婆
-
獲得老婆資訊
優缺點
- 優點:效能高,執行緒安全,延時載入
- 缺點:由於JVM底層模型,volatile不能完全避免指令重排的情況,會偶爾出現問題,反射、序列化會破壞雙檢索單例。
7、懶漢式(靜態內部類)
參考程式碼
/**
* 一個男人只能有一個老婆wife -> Wife類實現單例模式
* 方式7:懶漢式單例模式(靜態內部類,執行緒安全,效能高)
*/
public class Husband {
private Husband() {}
private static class Wife {
private static Husband husband = new Husband();
}
public static Husband getInstance() {
return Wife.husband;
}
}
細節說明
- 靜態內部類實現的單例模式同樣是懶漢式 -> 外部類載入時並沒有建立好物件,只有呼叫特定方法時才會載入靜態內部類資訊(內部類的靜態物件屬性建立完畢)
- 靜態內部類的方式實現的單例模式:執行緒安全(只有在第一次載入內部類資訊時才會建立物件),效率高(不需要獲取互斥鎖)
優缺點
- 優點:執行緒安全,效率高,實現了延遲載入
- 缺點:只適合簡單的物件例項,需要建立的物件例項有複雜操作時(如要對 物件例項 進行其他賦值操作),程式碼會更復雜。反射會破壞單例模式。
8、餓漢式(列舉)
參考程式碼
enum Husband {
HUSBAND;
}
說明:除了第8種列舉類實現單例模式,其他七種模式都會被反射、序列化破壞單例模式(因為反射可以獲得類的私有屬性的構造器),只有列舉類實現的單例不會被反射破壞,反射無法獲取到列舉類的構造方法。
優缺點
- 優點:執行緒安全,呼叫效率高,不會被反射、序列化破壞列舉單例
- 缺點:不能延時載入