Singleton Pattern 單例模式,作為創建型模式的一種,其保證了類的實體物件只有一個,并對外提供此唯一實體的訪問介面

概述
對于單例模式而言,其最核心的目的就是為了保證該類的實體物件是唯一的,為此一方面,需要將該類的建構式設為private,另一方面,該類需要在內部完成實體的構造并對外提供訪問介面,單例模式的好處顯而易見,可以避免頻繁創建、銷毀實體所帶來的性能開銷;但其缺點也同樣明顯,此類不僅需要描述業務邏輯,同時還需要構造出該類的唯一物件并對外提供訪問介面,其顯然違背了單一職責原則
實作
單例模式的思想雖然簡單易懂,但實作起來卻可謂是花樣繁多、妙不可言,這里來介紹幾種常見的單例模式的實作
餓漢式
如下實作最為簡單,當 SingletonDemo1 類被加載到JVM中,即會完成實體化,即不是所謂的Lazy Load 延遲加載,故通常被稱之為 “餓漢式” 單例,其最大的問題就在,可能構造出來的實體物件從頭到尾沒有被使用過(沒有呼叫過getInstance方法),從而浪費記憶體,可能有人會對此有些困惑,SingletonDemo1 類被加載到JVM中了,那肯定是因為呼叫了getInstance方法啊,難道還有別的原因?答案是肯定的
這里,我們先簡要補充一些類加載機制的相關知識點,我們知道Java中的類被加載到JVM中,通常會有如下幾個階段:加載、 驗證、準備、決議、初始化等,其中對于初始化階段而言,虛擬機規范嚴格規定了有且僅有以下5種情況必須立即對類進行初始化(而加載、 驗證、準備顯然必須在此之前開始):
- 遇到new、getstatic、putstatic或invokestatic型別的位元組碼指令時,在Java代碼層面上就是new物件、讀取或設定類的靜態變數(被final修飾、已在編譯期將結果放入常量池的靜態變數除外)、呼叫類的靜態方法
- 對該類使用反射
- 當初始化一個類的時候,如果發現其父類還未初始化,則需要先初始化父類
- 當JVM啟動時,虛擬機會先初始化開發者所指定的主類(即main方法所在類)
- 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實體最后決議的結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且該方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化
說到這里,大家可能就明白了,如果SingletonDemo1類中還有其他靜態方法,一旦被呼叫就會導致SingletonDemo1類被加載、初始化,此時即完成了實體的構造,眾所周知,JVM保證了類加載程序的執行緒安全,所以餓漢式單例同樣是執行緒安全的
/**
* 單例模式1,餓漢式
*/
public class SingletonDemo1 {
private static SingletonDemo1 instance = new SingletonDemo1("我是餓漢式的單例");
private String description;
/**
* 私有構造器
* @param description
*/
private SingletonDemo1(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供實體的訪問介面
* @return
*/
public static SingletonDemo1 getInstance() {
return instance;
}
public static void main(String[] args) {
SingletonDemo1 singletonDemo1 = SingletonDemo1.getInstance();
singletonDemo1.getInfo();
}
}
測驗結果如下所示

懶漢式
前面說到,餓漢式單例會導致記憶體空間的浪費,那么有沒有辦法解決這個問題呢?答案是有的,這就是”懶漢式”單例,顧名思義,其實體不是在類加載、初始化時被構建的,而是在真正需要的時候才去創建,如下所示
/**
* 單例模式2,執行緒不安全的懶漢式
*/
public class SingletonDemo2 {
private static SingletonDemo2 instance = null;
private String description;
private SingletonDemo2(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
public static SingletonDemo2 getInstance() {
if( instance==null ) {
instance = new SingletonDemo2("我是執行緒不安全的懶漢式單例");
}
return instance;
}
public static void main(String[] args) {
SingletonDemo2 singletonDemo2 = SingletonDemo2.getInstance();
singletonDemo2.getInfo();
}
}
測驗結果如下所示

“懶漢式”單例雖然實作了Lazy Load延遲加載,但是其存在一個很嚴重的問題,不是執行緒安全的,所以如果在多執行緒環境下,我們需要使用下面執行緒安全的”懶漢式”單例,其保障執行緒安全的手段也很簡單,直接使用synchronized來修飾getInstance方法,這種辦法過于簡單粗暴,同時會導致效率十分低下,實體一旦被構造完畢后,由于鎖的存在,導致每次只能由一個執行緒可以獲取到實體物件
/**
* 單例模式3, 執行緒安全但效率低下的懶漢式
*/
public class SingletonDemo3 {
private static SingletonDemo3 intance = null;
private String description;
private SingletonDemo3(String description) {
this.description = description;
}
public void getInfo() {
System.out.printf(description);
}
public static synchronized SingletonDemo3 getInstance() {
if( intance==null ) {
intance = new SingletonDemo3("我是執行緒安全執行緒安全但效率低下的懶漢式單例");
}
return intance;
}
public static void main(String[] args) {
SingletonDemo3 singletonDemo3 = SingletonDemo3.getInstance();
singletonDemo3.getInfo();
}
}
測驗結果如下所示

基于DCL(Double-Checked Locking)雙重檢查鎖的單例
通過前面我們看到,無論是餓漢式單例還是懶漢式單例,其都有明顯的缺點,那么有沒有一種完美的單例?既可以實作Lazy Load延遲加載,又可以在保證執行緒安全的前提下依然具備較高的效率呢,答案是肯定——基于DCL(Double-Checked Locking)雙重檢查鎖的單例,其實作如下,該單例實作中進行了兩次檢查,第一次檢查時如果發現實體已經構造完畢了,則無需加鎖直接回傳實體物件即可,其保證了實體在構建完成后,其他多個執行緒可以同時快速獲取該實體,第二次檢查時則是為了避免重復構造實體,因為在還未構造實體前,可能會有多個執行緒通過了第一次檢查,準備加鎖來構造實體,在DCL的單例實作中,尤其需要注意的一點是靜態變數instance必須要使用volatile進行修飾,其原因在于volatile禁止了指令的重排序,這里就此問題再作一些詳細的解釋說明:在JDK1.5之前的Java記憶體模型中,雖然不允許volatile變數之間進行重排序,但卻允許普通變數與volatile變數之間的重排序,所以在JSR 133(JDK 1.5)中對volatile變數的記憶體語意進一步增強,即限制了普通變數與volatile變數之間是否可以重排序的具體場景,這也是為什么在JDK 1.5之前無法通過DCL實作一個執行緒安全的單例模式
/**
* 單例模式4,基于DCL的執行緒安全的單例
*/
public class SingletonDemo4 {
// 此處必須要使用volatile修飾!
private static volatile SingletonDemo4 instance = null;
private String description;
private SingletonDemo4(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
public static SingletonDemo4 getInstance() {
if( instance==null ) { // 第一次檢查:如果實體已經構造完成則直接取,避免每次取之前需要獲取鎖
synchronized (SingletonDemo4.class) {
if(instance==null) { // 第二次檢查:避免構造出多個實體
instance = new SingletonDemo4("我是基于DCL的執行緒安全的單例");
}
}
}
return instance;
}
public static void main(String[] args) {
SingletonDemo4 singletonDemo4 = SingletonDemo4.getInstance();
singletonDemo4.getInfo();
}
}
測驗結果如下

基于靜態內部類的單例
前面我們說到的第一種單例實作,之所以被稱為餓漢式、非延遲加載,其原因就在于類的加載、初始化不能100%保證是因為呼叫getInstance方法引起的,而這里我們通過靜態內部類的方式來實作一個延遲加載的單例,代碼如下所示,當呼叫外部類SingletonDemo5的一些靜態方法(當然getInstance方法除外),只會加載、初始化外部類SingletonDemo5,而不會去初始化靜態內部類SingletonDemo5Holder,只有通過呼叫getInstance方法訪問了靜態內部類SingletonDemo5Holder的靜態變數instance,靜態內部類SingletonDemo5Holder才會被加載、初始化,顯然此時實體才會被真正的構造,所以對于基于靜態內部類的單例實作而言,其之所以能保證Lazy Load延遲加載特性,是其因為通過SingletonDemo5Holder靜態內部類100%保證了靜態內部類被加載、初始化是因為呼叫外部類的getInstance方法而導致的,同樣地,該方式的單例也是滿足執行緒安全的,原因在餓漢式單例實作中已作解釋,此處就不再贅述
/**
* 單例模式5,靜態內部類
*/
public class SingletonDemo5 {
private String description;
private SingletonDemo5(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
private static class SingletonDemo5Holder{
private static final SingletonDemo5 instance = new SingletonDemo5("我是基于靜態內部類的執行緒安全的單例");
}
public static SingletonDemo5 getInstance() {
return SingletonDemo5Holder.instance;
}
public static void main(String[] args) {
SingletonDemo5 singletonDemo5 = SingletonDemo5.getInstance();
singletonDemo5.getInfo();
}
}
測驗結果如下所示

基于列舉的單例
對于Java的列舉型別而言,其構造器是且只能是private私有的,故其特別適合用于實作單例模式,下面即是一個基于列舉的單例實作,可以看到此種實作非常簡潔優雅,當列舉類進行加載、初始化時,即會完成實體的構建,我們通過列舉的特性保證了實體的唯一性,當然其不是Lazy Load延遲加載的,與此同時根據類的加載機制我們可知其也是執行緒安全的(由JVM保證)
/**
* 單例模式6,列舉法
*/
public enum SingletonDemo6 {
INSTANCE("我是列舉法的單例");
private String description;
/**
* 列舉的構造器默認訪問權限是private, 當然也只能是私有的
* @param description
*/
SingletonDemo6(String description) {
this.description = description;
}
public void getInfo() {
System.out.println(description);
}
}
...
/**
* 測驗用例
*/
public class SingletonDemo6Test {
public static void main(String[] args) {
SingletonDemo6 singletonDemo6 = SingletonDemo6.INSTANCE;
singletonDemo6.getInfo();
}
}
測驗結果如下

參考文獻
- Head First 設計模式 弗里曼著
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/506160.html
標籤:其他
