單例模式作為設計模式中最常見最重要的設計模式,今天波吉帶你由淺入深的明白單例模式,相信你一定會有所識訓的
單例模式
簡介
? 所謂類的單例設計模式,就是采取一定的方法保證在整個的軟體系統中,對某個類只能存在一個物件實體,并且該類只提供了一個取得其物件實體的方法,如果我們要讓類在一個虛擬機中只能產生一個物件,我們首先必須將類的構造器的方法權限設定為private,這樣就不能用new運算子在類的外部產生類的物件了,但是類內部仍然可以產生該類的物件,因為在類的外部開始還沒發得到類的物件,只能呼叫該類的某個靜態方法以回傳類內部構建的物件,靜態方法只能訪問類中的靜態成員變數,所以指向類內部產生的該類物件的變數也必須定義為靜態的
核心作用
保證一個類只有一個物件,并且提供一個訪問該實體的全域訪問點
優點
- 由于單例模式只生成一個實體,減少了系統的開銷,當一個物件需要比較的多的資源時,如讀取配置、產生其它依賴物件時,則可以通過在應用啟動時直接產生一個單例物件,然后永久駐留記憶體的方式來解決
- 單例模式可以在系統設定全域訪問點,優化環共享資源訪問,例如可以設計一個單例類,負責所以資料表的映射處理
應用場景
? 單例模式只生成了一個實體,減少了系統性能開銷,當一個物件的產生需要比較多的資源時,如讀取配置,產生其它依賴物件,則可以通過在應用啟動時直接產生一個單例物件,然后永久駐留記憶體的方式來解決,例如:java.lang.Runtime,網站計數器,應用程式的日志應用,資料庫連接池,讀取組態檔的類,Application,Windows中的任務管理器和回收站
- 業務系統全域只需要一個物件實體,比如發號器,redis連接物件等
- Spring IOC容器中的Bean默認就是單例
- Spring Boot中的Controller,Service,Dao層中 通過
@AutoWrie的依賴注入默認就是單例的
主要分類
- 餓漢:就是所謂的懶加載,延遲創建物件,需要使用的時候再延時創建物件,因為餓漢單例即使沒有呼叫方法也會占據記憶體所以我們一般不采用
- 懶漢:懶漢由于呼叫實體方法物件才會生成所以更符合我們的需求
餓漢單例
執行緒安全
class Bank {
//1.私有化構造器,不允許外部可以呼叫
private Bank() {
}
//2.內部創建類的物件
//4.要求此物件必須宣告為靜態
private static Bank instance = new Bank();
//3.提供公共的方法,回傳類的物件
public static Bank getInstance() {
return instance;
}
}
public class SingletonTest1 {
public static void main(String[] args) {
Bank bank = Bank.getInstance();
}
}
餓漢模式簡單了解后我們著重分析懶漢單例
單例模式實作步驟:
- 私有化建構式,
- 提供獲取單例的?法,
懶漢單例
這是最簡單實作懶漢單例的實體,后續內容會對懶漢單例做出升級!在本次實體中執行緒不安全
/**
* 單例設計模式——懶漢
*
* @author ccy
* @version 1.0
* @date 2021/12/6 13:18
*/
class Order {
//1.私有化構造器,不允許外部可以呼叫
private Order() {
}
//2.宣告當前類的物件
//4.要求此物件必須宣告為靜態
private static Order instance = null;
//3.提供公共的方法,回傳類的物件
public static Order getInstance() {
if (instance == null) {
instance = new Order();
}
return instance;
}
}
標題寫到這種情況的單例模式是執行緒不安全原因就在于在高并發的場景下會創建多個物件違背了單例模式只創建一次的情況

為了應對高并發下能夠只創建一次物件的情況所以我們引入Synchroized,但是采用synchronized 對方法加鎖有很大的性能開銷,因為當getInstance()內部邏輯比較復雜的時候,在高并發條件下沒獲取到加鎖方法執行權的執行緒,都得等到這個方法內的復雜邏輯執行完后才能執行,等待浪費時間,效率比較低
這種實作懶漢單例執行緒是安全的但是不采用它的原因就在于synchronized帶來的效率低
class Order {
//1.私有化構造器,不允許外部可以呼叫
private Order() {
}
//2.宣告當前類的物件
//4.要求此物件必須宣告為靜態
private static Order instance = null;
//3.提供公共的方法,回傳類的物件
public synchronized static Order getInstance() {
if (instance == null) {
instance = new Order();
}
return instance;
}
}
為了滿足以上需求,DCL雙重檢測鎖機制的單例模式就出現了
雙重檢測鎖模式
下面是上述代碼的運行順序:
- 檢測實體是否已經初始化創建,如果是則立即回傳
- 獲得鎖
- 再次檢測實體是否已經初始化創建成功,如果還沒有則創建實體
/**
* 單例設計模式——懶漢
*
* @author ccy
* @version 1.0
* @date 2021/12/6 13:18
*/
class Order {
//1.私有化構造器,不允許外部可以呼叫
private Order() {
}
//2.宣告當前類的物件
//4.要求此物件必須宣告為靜態
private static Order instance = null;
//3.提供公共的方法,回傳類的物件
public static Order getInstance() {
if (instance == null) {
synchronized (Order.class) {
if (instance == null) {
instance = new Order();
}
}
}
return instance;
}
}
public class SingletonTest2 {
public static void main(String[] args) {
Order order1 = Order.getInstance();
Order order2 = Order.getInstance();
System.out.println(order1 == order2);
}
}
DCL雙重檢測鎖機制在邏輯上的確是趨近于完美了但是!!!由于指令重排的原因仍然有可能會創建多個物件,因為instance = new Order()這行代碼的執行邏輯是
- 在堆中開辟物件所需空間,分配地址
- 根據類加載的初始化順序進行初始化
- 將記憶體地址回傳給堆疊中的參考變數
由于指令重排(你可以把它理解成編譯器為了優化代碼而對實際寫出的代碼在機器中進行重新排序導致原本的代碼執行順序發生變化)的緣故會出現這樣的情況
- 在堆中開辟物件所需空間,分配地址
- 將記憶體地址回傳給堆疊中的參考變數(此時變數已不為null,但是變數卻并沒有初始化完成)
- 根據類加載的初始化順序進行初始化
在多執行緒下指令重排給帶來的問題就會被放大
| 執行順序 | Thread1 | Thread2 |
|---|---|---|
| 1 | 第一次檢測, instance 為null | |
| 2 | 獲取鎖 | |
| 3 | 第二次檢測, instance 為null | |
| 4 | 在堆中分配記憶體空間 | |
| 5 | instance 指向分配的記憶體空間 | |
| 6 | 第一次檢測,instance不為null | |
| 7 | 此時Thread 2對 instance的訪問,訪問到的是一個還未完成初始化的物件,所以在使用 instance 時可能會出錯 | |
| 8 | 初始化 instance |
所以為了避免指令重排我們只需要對初始化物件加volatile這一關鍵字即可,volatile是可以預防指令重排
下面代碼是正確的雙重檢測鎖機制
/**
* 正確的雙重檢測鎖機制
*
* @author ccy
* @version 1.0
* @date 2021/12/6 13:18
*/
class Order {
//1.私有化構造器,不允許外部可以呼叫
private Order() {
}
//2.宣告當前類的物件
//4.要求此物件必須宣告為靜態
private volatile static Order instance = null; // 注意哦這里添加了關鍵字volatile為了防止指令重排
public static Order getInstance() {
if (instance == null) {
synchronized (Order.class) {
if (instance == null) {
instance = new Order();
}
}
}
return instance;
}
}
雖然這種單例模式執行緒安全雖然構造方法是私有的但是Java中反射可以破壞私有方法,我們仍然可以通過反射來獲取物件
反射破壞雙重檢測鎖機制
public class Lazy {
private Lazy() {
System.out.println(Thread.currentThread().getName());
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if(lazy == null) {
synchronized (Lazy.class) {
if(lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) throws Exception {
Lazy instance1 = Lazy.getInstance();
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Lazy instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
}
}

列舉
沒辦法了只能搬出殺手锏了列舉元素天生就是執行緒安全的單例,呼叫效率也高,只是無法延時加載!
main方法中是我嘗試使用反射來破壞列舉單例
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
}
//嘗試使用反射破壞列舉單例
class TestSingle {
public static void main(String[] args) throws Exception {
EnumSingle instance = EnumSingle.getInstance();
// EnumSingle instance1 = EnumSingle.INSTANCE;
// System.out.println(instance == instance1);
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance == instance2);
}
}
雖然IDEA報錯了但是錯誤并不是我們預計的錯誤

但從.class檔案來看確實是存在無參構造的方法經過一系列手段后我們了解到實際上呼叫的是有參的構造方法
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);調整代碼以后報錯成了我們預計的

單例模式我們今天就學到這里吧,如果博文對你有幫助

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/393130.html
標籤:java
上一篇:【設計模式】單例模式

