目錄
一、單例模式概述
二、“餓漢模式”實現單例模式
三、“懶漢模式”實現單例模式
3.1 單線程下的“懶漢模式”
3.2 多線程下的“懶漢模式”
一、單例模式概述
1)什么是單例模式? |
單例模式是一種設計模式。 單例模式可以保證某個類在程序中只存在唯一實例,即不允許創建多份實例。 使用單例模式,上述要求就得到了檢查和校驗。 |
2)單例模式的實現形式 |
單例模式可以通過很多種方法實現,“餓漢模式”和“懶漢模式”是其中最基礎的兩種,本文只介紹這兩種實現。 |
二、“餓漢模式”實現單例模式
通過代碼演示“餓漢模式”實現的單例模式:
class Singleton{//新建一個唯一實例;private static Singleton instance = new Singleton();//方法返回唯一實例;public static Singleton getInstance() {return instance;}//將構造方法私有化;private Singleton() { }
}
1)上述代碼做了什么? |
創建了一個被?static 修飾的實例,這個實例成為了類屬性。類對象只會有一個,這個類屬性也只會有一個。 |
私有化構造方法,外部無法 new 新的實例,只能通過 get 方法獲取唯一的那一個 instance。 |
2)為什么叫做“餓漢模式”? |
上述代碼中,實例是類屬性。類屬性在類加載的時候就創建了,創建時機早,十分“迫切”,因此稱為“餓漢模式”。 |
代碼證明“餓漢模式”返回的實例是唯一的:
public class Singleton_Demo0 {public static void main(String[] args) {//想直接new對象,就會報錯;//Singleton instance = new Singleton();//兩次調用getInstance()方法并分別賦值;Singleton instance1 = Singleton.getInstance();Singleton instance2 = Singleton.getInstance();//對比兩個變量,發現是同一實例;if(instance1 == instance2){System.out.println("兩個對象是同一個對象");}}
}//運行結果:
兩個對象是同一個實例
3)“餓漢模式”的單例模式在多線程下是線程安全的嗎? |
上述代碼中,get 方法返回的是已經創建好的實例,這個操作本質上只是一個“讀操作”,多個線程讀取同一個變量并不會造成線程不安全。 因此“餓漢模式”的單例模式在多線程下是線程安全的。 |
三、“懶漢模式”實現單例模式
3.1 單線程下的“懶漢模式”
通過代碼演示“懶漢模式”實現的單例模式:
class Singleton{//聲明一個變量作為類屬性;private static Singleton instance = null;//判斷變量是否為null,是則創建實例后返回,否則返回;public static Singleton getInstance() {if(instance == null){instance = new Singleton();}return instance;}//將構造方法私有化;private Singleton() { }
}
1)上述代碼做了什么? |
聲明了一個類屬性。類對象只會有一個,這個類屬性也只會有一個。 |
私有化構造方法,外部無法 new 新的實例,只能通過 get 方法獲取唯一的那一個 instance。 |
get 方法中根據變量是否為 null 判斷是否應該創建實例。 |
2)為什么叫做“懶漢模式”? |
上述代碼中,實例是在程序員第一次調用 get 方法后才創建的,創建時機較晚,或者根本不用創建,因此稱為“懶漢模式”。 |
3.2 多線程下的“懶漢模式”
1)單線程下的“懶漢模式”在多線程下是線程安全的嗎? | ||
答案是否定的,單線程下的“懶漢模式”在多線程下是線程不安全的,我們可以從以下兩個方面分析: | ||
“原子性”: 上述代碼中判斷變量是否為空的代碼 —— if(instance == null),和實例化代碼 ——? instance = new Singleton(),并非是“原子”的。在多線程環境下,這就可能導致線程不安全。 可以使用 synchronized 關鍵字,將這兩句代碼加鎖,解決這個問題。 | ||
內存可見性和指令重排序: 因為 instance 是一個被?static 修飾的共享數據,而且編譯器內部可能對實例化的代碼 ——?new Singleton(),進行了編譯器優化。 這就無法保證內存的可見性和指令的順序執行,因此在多線程環境下可能導致線程不安全。 可以使用 volatile 關鍵字,對共享數據?instance 進行修飾,解決這個問題。 |
使用以上兩個關鍵字的原因和方式,詳細請參考以下博客:
閱讀指針 -> 《synchronized 關鍵字 和?volatile 關鍵字》<JavaEE> synchronized關鍵字和鎖機制 -- 鎖的特點、鎖的使用、鎖競爭和死鎖、死鎖的解決方法-CSDN博客文章瀏覽閱讀70次。介紹了 synchronized 關鍵字 和 鎖機制,其中重點介紹了鎖的特點、使用方法和死鎖的相關內容。https://blog.csdn.net/zzy734437202/article/details/134742168<JavaEE> volatile關鍵字 -- 保證內存可見性、禁止指令重排序-CSDN博客文章瀏覽閱讀59次。簡單介紹什么是內存可見性和指令重排序。volatile關鍵字可以將這兩種編譯器優化強制關閉。
https://blog.csdn.net/zzy734437202/article/details/134757070
2)“懶漢模式”在多線程下應該怎么編寫? | ||
根據上述分析,根據單線程模式下的“懶漢模式”進行改進。 方法如下: 增加?volatile 關鍵字對共享數據進行修飾。 為判斷是否為 null 和 實例化的代碼加鎖,使這兩句代碼稱為“原子”。 |
增加?volatile 關鍵字對共享數據進行修飾:
private volatile static Singleton instance = null;
為判斷是否為 null 和 實例化的代碼加鎖,使這兩句代碼稱為“原子”:
public static Singleton getInstance() {synchronized (locker){if(instance == null){instance = new Singleton();}}return instance;}
3)“雙重校驗鎖” | ||
我們再仔細分析一下上述的 get 方法。 假設程序需要多次調用這個 get 方法,那么每一次進入都會進行加鎖,加鎖是會增加系統開銷的。 那么是否真的有必要每次都加鎖呢? 當 get 方法被第一次調用,實例就會被創建,那么后續再調用這個 get 方法時,返回實例就好了,加鎖部分的代碼塊,完全可以不用執行。 在加鎖的代碼塊之外,再增加一個if(instance == null)進行判斷,那么實例在被創建之后,也就不會再進入加鎖的代碼塊中了。 我們成功利用“雙重校驗鎖”,優化了程序。 |
代碼演示“雙重校驗鎖”優化后的 get 方法:
public static Singleton getInstance() {//這個if用于判斷是否需要加鎖;if(instance == null){synchronized (locker){//這個if用于判斷是否需要新建實例;if(instance == null){instance = new Singleton();}}}return instance;}
經過以上的完善和優化,我們終于可以寫出在多線程下保證線程安全的“懶漢模式”單例模式了:
class Singleton{//聲明一個變量作為類屬性;private volatile static Singleton instance = null;private static final Object locker = new Object();//判斷變量是否為null,是則創建實例后返回,否則返回;public static Singleton getInstance() {//這個if用于判斷是否需要加鎖;if(instance == null){synchronized (locker){//這個if用于判斷是否需要新建實例;if(instance == null){instance = new Singleton();}}}return instance;}//將構造方法私有化;private Singleton() { }
}
閱讀指針 -> 《經典設計模式之 -- 使用阻塞隊列實現“生產者-消費者模型”》
<JavaEE> 經典設計模式之 -- 使用阻塞隊列實現“生產者-消費者模型”-CSDN博客自己實現了的阻塞隊列,介紹了經典的設計模式“生產者-消費者模型”。https://blog.csdn.net/zzy734437202/article/details/134807241