你寫的的單例模式多線程下是否安全?是否懶載入?如何防止反射破壞單例模式?閱讀本文,解鎖單例模式的更多細節。 ...
單例模式可以說只要是一個合格的開發都會寫,但是如果要深究,小小的單例模式可以牽扯到很多東西,比如:多線程是否安全?是否懶載入?性能等等。還有你知道幾種單例模式的寫法呢?如何防止反射破壞單例模式?
一、 單例模式
1.1 定義
單例模式就是在程式運行中只實例化一次,創建一個全局唯一對象。有點像 Java
的靜態變數,但是單例模式要優於靜態變數:
- 靜態變數在程式啟動的時候
JVM
就會進行載入,如果不使用,會造成大量的資源浪費; - 單例模式能夠實現懶載入,能夠在使用實例的時候才去創建實例。
開發工具類庫中的很多工具類都應用了單例模式,比例線程池、緩存、日誌對象等,它們都只需要創建一個對象,如果創建多份實例,可能會帶來不可預知的問題,比如資源的浪費、結果處理不一致等問題。
1.2 單例的實現思路
- 靜態化實例對象;
- 私有化構造方法,禁止通過構造方法創建實例;
- 提供一個公共的靜態方法,用來返回唯一實例。
1.3 單例的好處
- 只有一個對象,記憶體開支少、性能好;
- 避免對資源的多重占用;
- 在系統設置全局訪問點,優化和共用資源訪問。
二、 單例模式的實現
- 餓漢模式
- 懶漢模式
- 雙重檢查鎖模式
- 靜態內部類單例模式
- 枚舉類實現單例模式
2.1 餓漢模式
在定義靜態屬性時,直接實例化了對象
public class HungryMode {
/**
* 利用靜態變數來存儲唯一實例
*/
private static final HungryMode instance = new HungryMode();
/**
* 私有化構造函數
*/
private HungryMode(){
// 裡面可以有很多操作
}
/**
* 提供公開獲取實例介面
* @return
*/
public static HungryMode getInstance(){
return instance;
}
}
2.1.1 優點
由於使用了static
關鍵字,保證了在引用這個變數時,關於這個變數的所以寫入操作都完成,所以保證了JVM
層面的線程安全
2.1.2 缺點
不能實現懶載入,造成空間浪費:如果一個類比較大,我們在初始化的時就載入了這個類,但是我們長時間沒有使用這個類,這就導致了記憶體空間的浪費。
所以,能不能只有用到
getInstance()
方法,才會去初始化單例類,才會載入單例類中的數據。所以就有了:懶漢式。
2.2 懶漢模式
懶漢模式是一種偷懶的模式,在程式初始化時不會創建實例,只有在使用實例的時候才會創建實例,所以懶漢模式解決了餓漢模式帶來的空間浪費問題。
2.2.1 懶漢模式的一般實現
public class LazyMode {
/**
* 定義靜態變數時,未初始化實例
*/
private static LazyMode instance;
/**
* 私有化構造函數
*/
private LazyMode(){
// 裡面可以有很多操作
}
/**
* 提供公開獲取實例介面
* @return
*/
public static LazyMode getInstance(){
// 使用時,先判斷實例是否為空,如果實例為空,則實例化對象
if (instance == null) {
instance = new LazyMode();
}
return instance;
}
}
但是這種實現在多線程的情況下是不安全的,有可能會出現多份實例的情況:
if (instance == null) {
instance = new LazyMode();
}
假設有兩個線程同時進入到上面這段代碼,因為沒有任何資源保護措施,所以兩個線程可以同時判斷的 instance
都為空,都將去初始化實例,所以就會出現多份實例的情況。
2.2.2 懶漢模式的優化
我們給getInstance()
方法加上synchronized
關鍵字,使得getInstance()
方法成為受保護的資源就能夠解決多份實例的問題。
public class LazyModeSynchronized {
/**
* 定義靜態變數時,未初始化實例
*/
private static LazyModeSynchronized instance;
/**
* 私有化構造函數
*/
private LazyModeSynchronized(){
// 裡面可以有很多操作
}
/**
* 提供公開獲取實例介面
* @return
*/
public synchronized static LazyModeSynchronized getInstance(){
/**
* 添加class類鎖,影響了性能,加鎖之後將代碼進行了串列化,
* 我們的代碼塊絕大部分是讀操作,在讀操作的情況下,代碼線程是安全的
*
*/
if (instance == null) {
instance = new LazyModeSynchronized();
}
return instance;
}
}
2.2.3 懶漢模式的優點
實現了懶載入,節約了記憶體空間。
2.2.4 懶漢模式的缺點
- 在不加鎖的情況下,線程不安全,可能出現多份實例;
- 在加鎖的情況下,會使程式串列化,使系統有嚴重的性能問題。
懶漢模式中加鎖的問題,對於getInstance()
方法來說,絕大部分的操作都是讀操作,讀操作是線程安全的,所以我們沒必讓每個線程必須持有鎖才能調用該方法,我們需要調整加鎖的問題。由此也產生了一種新的實現模式:雙重檢查鎖模式。
2.3 雙重檢查鎖模式
2.3.1 雙重檢查鎖模式的一般實現
public class DoubleCheckLockMode {
private static DoubleCheckLockMode instance;
/**
* 私有化構造函數
*/
private DoubleCheckLockMode(){
}
/**
* 提供公開獲取實例介面
* @return
*/
public static DoubleCheckLockMode getInstance(){
// 第一次判斷,如果這裡為空,不進入搶鎖階段,直接返回實例
if (instance == null) {
synchronized (DoubleCheckLockMode.class) {
// 搶到鎖之後再次判斷是否為空
if (instance == null) {
instance = new DoubleCheckLockMode();
}
}
}
return instance;
}
}
雙重檢查鎖模式解決了單例、性能、線程安全問題,但是這種寫法同樣存在問題:在多線程的情況下,可能會出現空指針問題,出現問題的原因是JVM
在實例化對象的時候會進行優化和指令重排序操作。
2.3.2 什麼是指令重排?
private SingletonObject(){
// 第一步
int x = 10;
// 第二步
int y = 30;
// 第三步
Object o = new Object();
}
上面的構造函數SingletonObject()
,JVM
會對它進行指令重排序,所以執行順序可能會亂掉,但是不管是那種執行順序,JVM
最後都會保證所以實例都完成實例化。 如果構造函數中操作比較多時,為了提升效率,JVM
會在構造函數裡面的屬性未全部完成實例化時,就返回對象。雙重檢測鎖出現空指針問題的原因就是出現在這裡,當某個線程獲取鎖進行實例化時,其他線程就直接獲取實例使用,由於JVM
指令重排序的原因,其他線程獲取的對象也許不是一個完整的對象,所以在使用實例的時候就會出現空指針異常問題。
2.3.3 雙重檢查鎖模式優化
要解決雙重檢查鎖模式帶來空指針異常的問題,只需要使用volatile
關鍵字,volatile
關鍵字嚴格遵循happens-before
原則,即:在讀操作前,寫操作必須全部完成。
public class DoubleCheckLockModelVolatile {
/**
* 添加volatile關鍵字,保證在讀操作前,寫操作必須全部完成
*/
private static volatile DoubleCheckLockModelVolatile instance;
/**
* 私有化構造函數
*/
private DoubleCheckLockModelVolatile(){
}
/**
* 提供公開獲取實例介面
* @return
*/
public static DoubleCheckLockModelVolatile getInstance(){
if (instance == null) {
synchronized (DoubleCheckLockModelVolatile.class) {
if (instance == null) {
instance = new DoubleCheckLockModelVolatile();
}
}
}
return instance;
}
}
2.4 靜態內部類模式
靜態內部類模式也稱單例持有者模式,實例由內部類創建,由於 JVM
在載入外部類的過程中, 是不會載入靜態內部類的, 只有內部類的屬性/方法被調用時才會被載入, 並初始化其靜態屬性。靜態屬性由static
修飾,保證只被實例化一次,並且嚴格保證實例化順序。
public class StaticInnerClassMode {
private StaticInnerClassMode(){
}
/**
* 單例持有者
*/
private static class InstanceHolder{
private final static StaticInnerClassMode instance = new StaticInnerClassMode();
}
/**
* 提供公開獲取實例介面
* @return
*/
public static StaticInnerClassMode getInstance(){
// 調用內部類屬性
return InstanceHolder.instance;
}
}
這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不同的地方:
- 餓漢式方式是只要
Singleton
類被裝載就會實例化,沒有Lazy-Loading
的作用; - 靜態內部類方式在
Singleton
類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance()
方法,才會裝載SingletonInstance
類,從而完成Singleton
的實例化。
類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM
幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。
所以這種方式在沒有加任何鎖的情況下,保證了多線程下的安全,並且沒有任何性能影響和空間的浪費。
2.5 枚舉類實現單例模式
因為枚舉類型是線程安全的,並且只會裝載一次,設計者充分的利用了枚舉的這個特性來實現單例模式,枚舉的寫法非常簡單,而且枚舉類型是所用單例實現中唯一一種不會被破壞的單例實現模式。
public class EnumerationMode {
private EnumerationMode(){
}
/**
* 枚舉類型是線程安全的,並且只會裝載一次
*/
private enum Singleton{
INSTANCE;
private final EnumerationMode instance;
Singleton(){
instance = new EnumerationMode();
}
private EnumerationMode getInstance(){
return instance;
}
}
public static EnumerationMode getInstance(){
return Singleton.INSTANCE.getInstance();
}
}
適用場合:
- 需要頻繁的進行創建和銷毀的對象;
- 創建對象時耗時過多或耗費資源過多,但又經常用到的對象;
- 工具類對象;
- 頻繁訪問資料庫或文件的對象。
三、單例模式的問題及解決辦法
除枚舉方式外, 其他方法都會通過反射的方式破壞單例
3.1 單例模式的破壞
/**
* 以靜態內部類實現為例
* @throws Exception
*/
@Test
public void singletonTest() throws Exception {
Constructor constructor = StaticInnerClassMode.class.getDeclaredConstructor();
constructor.setAccessible(true);
StaticInnerClassMode obj1 = StaticInnerClassMode.getInstance();
StaticInnerClassMode obj2 = StaticInnerClassMode.getInstance();
StaticInnerClassMode obj3 = (StaticInnerClassMode) constructor.newInstance();
System.out.println("輸出結果為:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}
控制台列印:
輸出結果為:1454171136,1454171136,1195396074
從輸出的結果我們就可以看出obj1
和obj2
為同一對象,obj3
為新對象。obj3
是我們通過反射機制,進而調用了私有的構造函數,然後產生了一個新的對象。
3.2 如何阻止單例破壞
可以在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法如下:
public class StaticInnerClassModeProtection {
private static boolean flag = false;
private StaticInnerClassModeProtection(){
synchronized(StaticInnerClassModeProtection.class){
if(flag == false){
flag = true;
}else {
throw new RuntimeException("實例已經存在,請通過 getInstance()方法獲取!");
}
}
}
/**
* 單例持有者
*/
private static class InstanceHolder{
private final static StaticInnerClassModeProtection instance = new StaticInnerClassModeProtection();
}
/**
* 提供公開獲取實例介面
* @return
*/
public static StaticInnerClassModeProtection getInstance(){
// 調用內部類屬性
return InstanceHolder.instance;
}
}
測試:
/**
* 在構造方法中進行判斷,若存在則拋出RuntimeException
* @throws Exception
*/
@Test
public void destroyTest() throws Exception {
Constructor constructor = StaticInnerClassModeProtection.class.getDeclaredConstructor();
constructor.setAccessible(true);
StaticInnerClassModeProtection obj1 = StaticInnerClassModeProtection.getInstance();
StaticInnerClassModeProtection obj2 = StaticInnerClassModeProtection.getInstance();
StaticInnerClassModeProtection obj3 = (StaticInnerClassModeProtection) constructor.newInstance();
System.out.println("輸出結果為:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}
控制台列印:
Caused by: java.lang.RuntimeException: 實例已經存在,請通過 getInstance()方法獲取!
at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22)
... 35 more
四、總結
4.1 各種實現的對比
名稱 | 餓漢模式 | 懶漢模式 | 雙重檢查鎖模式 | 靜態內部類實現 | 枚舉類實現 |
---|---|---|---|---|---|
可用性 | 可用 | 不推薦使用 | 推薦使用 | 推薦使用 | 推薦使用 |
特點 | 不能實現懶載入,可能造成空間浪費 | 不加鎖線程不安全;加鎖性能差 | 線程安全;延遲載入;效率較高 | 避免了線程不安全,延遲載入,效率高。 | 寫法簡單;線程安全;只裝載一次 |