很多時候,我們在使用類創建類的實例并不想可以創建很多實例對象,比如在數據庫連接的時候,對于一個數據庫的連接通常只需要連接池中的某個連接的實例,連接一次即可,對于session會話,用戶在訪問網頁做會話保持的時候,一個用戶只需要一個實例來表示本次會話即可。
設計模式就像是下圍棋那樣的一些定式,棋譜,如果對方小飛掛角,我們可以選擇小飛守角,或者大飛守角等等,也就說如果一個棋力不足的新手能把一部分常用的定式或者棋譜背下來,那么遇到了類似的情況或者定式的招數就可以運用出來,而不至于下的一塌糊涂,也就是為新手菜狗提供了保底的手法。
1.單例模式
在23種典型的設計模式中就存在一種單例模式,可以很好的解決我們最初提到的,"全局單個實例"的場景,就是"單例模式"很有見名知意的感覺。
1.1 定義
單例模式在大佬給出的定義是:
通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,并且它可以提供一個訪問該實例的辦法。
1.提供全局變量使得一個對象被訪問是什么意思呢?
MyClass instance = new MyClass(); // 每次都可以創建新的實例
可以把instance設置為一個全局變量,比如
private static final MyClass instance = new MyClass();
?2.讓類自身負責保存它唯一的實例是什么意思?
這個類做到兩件事:
-
私有化構造方法,別人就不能隨便
new
它了; -
在類中自己創建并保存唯一的實例;
-
提供一個公開的靜態方法用于獲取該實例。
1.2 單例模式的使用
在單例模式這個定式中,存在兩種手法
1.餓漢式的單例模式
餓漢式就是,在提供全局變量的時候,就為這個全局變量創建一個實例,這個全局變量和實例一般設置為static,這樣他就會隨著類的加載就進行初始化。
public class Singleton {// 1. 提前創建好唯一實例(類加載時就實例化)private static final Singleton instance = new Singleton();// 2. 構造方法私有化,防止外部 newprivate Singleton() {}// 3. 提供靜態方法讓外部訪問實例public static Singleton getInstance() {return instance;}
}
2.懶漢式的單例模式
懶漢式就是,在提供全局變量的時候并不為這個全局變量創建實例,而是等到使用時在提供的全局訪問點,也就是提供的靜態方法去獲得實例的時候,在進行創建實例,這樣就減少了類加載時的一些初始化工作。
public class Singleton {private static Singleton instance;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton(); // 延遲加載}return instance;}
}
單例模式除了可以保證唯一的實例外,還有什么好處呢?
比如單例模式因為Singleton類封裝他的唯一實例,這樣它可以嚴格控制客戶怎么樣訪問它以及合適訪問它,簡單來說就是對唯一實例的受控訪問。
單例模式通過自己管理自己的唯一實例(比如通過
private static Singleton instance
),并只提供一個公開的獲取方法(如getInstance()
),從而實現對這個實例的訪問控制:
外部不能隨便
new
一個新的對象;外部必須通過你提供的方式來訪問;
類本身可以在需要的時候控制創建時機(比如懶漢式延遲創建);
這就叫“對唯一實例的受控訪問”。
單例模式看起來有點像實用類中的靜態方法,比如Math類有很多數學計算方法,他們之間雖然很類似,實用類通常也會采用私有化的構造方法來避免其有實例。但是他們還是有很多不同的
單例類和工具類(實用類)在結構上是有點像的,比如:
都私有了構造方法,不允許
new
;都通過類名來訪問功能(方法或實例);
比如
Math.abs(-1)
這樣的調用方式也不用創建對象,看起來就和Singleton.getInstance()
類似。
1.實用類不保存狀態,僅提供一些靜態方法或者靜態屬性來讓我們使用,單例模式卻是有狀態的。
2.實用類不能用于繼承多態,而單例模式雖然實例唯一,卻可以有子類來繼承
3.實用類只不過是一些方法屬性的集合,而單例模式確實有著唯一的對象實例。
2.多線程下的單例模式
很多代碼程序在單線程下運行的十分完美,但是到了多線程的環境下就會暴露出很多短板甚至是bug,比如上面的單例模式,在多個線程同時,注意是同時訪問Singleton類,調用getInstance方法是會有可能創建多個實例的。
很尷尬,那應該怎么解決呢?
線程安全問題的發現與解決-CSDN博客
我們在前面分析了,多線程下的線程安全問題,這種情況就屬于線程安全的問題之一,
是因為,修改操作不是原子的情況所造成的
比如下面的代碼
/*** 懶漢式單例模式*/
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){};public static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}
}
我們發現,在getInstance中并沒有像之前提到的count++這樣的修改操作呀
也就是有個?
if(instance == null)//判斷 instance = new SingletonLazy();//賦值 return instance;
返回操作是一種"讀操作"通常不會是多線程下bug的元兇
那么原因是因為if(instance == null)//判斷 或者 instance = new SingletonLazy();//賦值 再或者是二者合并起來造成的問題嗎?
Java中的賦值操作,確實本質上是一種"讀操作"也不應該是造成問題的原因,
原因是第三種情況,拆開各自安好,合并就可能會出現問題了,因為if(instance == null)//判斷 和instance = new SingletonLazy();//賦值 二者放在一起是一個完整的邏輯。
1.多線程改進1
?問題核心就是線程不安全導致重復實例化
我們不妨嘗試一下加鎖,讓不是原子性的操作變成加鎖后的原子性的操作
synchronized (SingletonLazy.class){if(instance == null){instance = new SingletonLazy();}}
之前我們討論解決線程安全時講過synchronized的使用
在此處的getInstance方法中,想要加鎖因為是靜態方法的緣故,就要使用當前類的Class對象來充當鎖對象。
如果想要使用實例的鎖對象也是可以的可以這樣寫代碼:
/*** 懶漢式單例模式*/
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){};private static final Object lock = new Object();public static SingletonLazy getInstance(){synchronized (lock){if(instance == null){instance = new SingletonLazy();}}return instance;}
}
關于鎖的問題,我們不再討論,現在我們來看一下加鎖后的效果
-
線程1 搶到了鎖,成功進入
synchronized (lock)
的同步塊; -
線程1 執行判斷:
instance == null
,結果為true
; -
線程1 開始創建單例對象(執行
new SingletonLazyO()
); -
此時線程2 也調用了
getInstance()
,但因為同步塊已經被線程1占用,所以線程2在 synchronized 外面等待; -
線程1 創建完實例后,退出同步塊,釋放了鎖,并且
instance
已經指向新建好的對象; -
線程2 被喚醒,獲取到了鎖,進入同步塊;
-
再次檢查
instance == null
,這次結果為false
(因為線程1已經創建好了); -
線程2 直接返回現有的實例,避免了重復創建。
-
最終,兩個線程都獲得了同一個對象實例;
-
沒有出現重復創建或資源浪費的問題;
-
符合單例模式“全局唯一實例”的設計目標;
-
這種方式雖然線程安全,但每次訪問都進入同步塊,性能稍差,可以通過雙重檢查優化
2.多線程改進2
我們知道加鎖是存在一定的代價的
為了避免每次都進入 synchronized
塊,可以使用“雙重檢查鎖”:?
public static SingletonLazy getInstance(){if(instance == null){synchronized (lock){if(instance == null){instance = new SingletonLazy();}}}return instance;}
初次見這種雙重if而且內外if條件還是相同的,很多新手會覺得代碼邏輯很混亂
if (instance == null)
— 第一次檢查(不加鎖)
-
這是性能優化的關鍵:
-
大多數時候,
instance
已經被創建了,不需要進入同步代碼塊; -
只有第一次創建的時候才需要同步;
-
避免每次都加鎖,提高效率。
-
synchronized (Singleton.class)
-
加鎖的對象是類的
.class
對象,因為instance
是靜態變量,是整個類共享的; -
保證只有一個線程可以創建實例;
-
是解決線程安全的核心。
if (instance == null)
— 第二次檢查(加鎖后再確認)
-
為什么要檢查兩次?
-
如果不再判斷一遍,多個線程可能都在排隊等鎖;
-
第一個線程創建了對象,釋放鎖后,后面的線程仍然會再創建一次,如果不檢查;
-
所以要加鎖后再檢查一次,防止重復創建。
-
instance = new Singleton();
-
真正創建對象的地方;
-
只有在加鎖的前提下,并且確認 instance 為 null 的時候才會執行。
3.多線程改進3
上面的代碼仍然存在一定的缺陷,我們還有一種很隱匿的缺陷沒有找到,那就是指令重排序的問題
線程安全問題的發現與解決-CSDN博客
前面我們提到,線程安全的幾大問題其中之一就是,修改操作不是原子的
new SingletonLazy()
這條語句看起來僅僅只是Java的一條普通的實例化語句,但是在JVM層面就包括了三個步驟
1.為該實例開辟內存空間,分配內存
2.初始化該實例對象
3.最后instance引用賦值,引用這一塊內存空間
編譯器會覺得,如果我快點引用,先不初始化能不能讓代碼執行的更快,更高效呢?
所以它大膽的調換了執行順序變成了
1.為該實例開辟內存空間,分配內存
3.instance引用賦值,引用這一塊內存空間
2.初始化該實例對象
這不換不要緊,一換的話,如果存在別的線程在“賦值”和“初始化之間”訪問這個對象,順便修改了就會造成bug。
假如線程 A 執行到
instance = new SingletonLazy();
,由于重排序:
它已經把 instance 指向了還“沒初始化”的對象
此時線程 B 也進來了,看到
instance != null
,以為已經初始化好了然后就直接拿這個對象用了(return instance)!結果呢?對象狀態是不完整的!
這就產生了嚴重的**“半初始化對象被訪問”**的問題
這里其實也說明了為什么單線程下指令重排序根本沒有問題
因為
不存在別的線程在“賦值”和“初始化之間”訪問這個對象,也就不存在bug。
3.多線程下單例模式的使用總結
綜上,很多代碼在單線程下生龍活虎,因為單線程下沒有其他線程來“搶時間”、“搶資源”,所以很多細節(比如原子性、可見性、重排序)根本不會暴露出來。這就是為什么并發編程下會出現很多的問題,所以我們在使用單例模式的多線程版本的時候,要記得以下兩點
-
使用雙重 if 判定(Double-Checked Locking)
避免每次獲取實例都加鎖,提高性能。 -
在實例變量上添加
volatile
關鍵字
防止 JVM 發生指令重排序,確保對象初始化的完整性。