Java設計模式之單例設計模式
1. 單例設計模式實現有八種方式:
- 餓漢式(靜態常量)
- 餓漢式(靜態代碼塊)
- 懶漢式(線程不安全)
- 懶漢式(線程安全,同步方法)
- 懶漢式(線程安全,同步代碼塊)
- 雙重檢查
- 靜態內部類
- 枚舉
2. 餓漢式(靜態常量)實現單例
2.1. 實現步驟:
(1) 構造器私有化
(2) 類的內部創建對象
(3) 向外暴露一個靜態的公共方法。
2.2 代碼實現
class Singleton {
// 1. 構造器私有化, 外部能new
private Singleton () {}
// 2. 內部可以創建對象實例
private final static Singleton instance = new Singleton();
// 3. 提供一個靜態方法,返回實例對象
public static Singleton getInstance() {
return instance;
}
}
2.3. 優缺點說明:
- 優點: 這種寫法比較簡單,就在類裝載的時候完成實例化,避免了線程同步問題。
- 缺點:在類裝載的時候就完成實例化,沒有達到
Lazy Loading
的效果。如果從始到終未曾使用這個類,則會導致內存浪費。 - 這種方式基於
classloader
機制避免了多線程同步問題,不過,instance
在類的裝載時就實例化,在單例模式中大多數都是調用getInstance
方法,但是導致類裝載的原因很多,因此不確定有其他方式導致類裝載,這時候初始化instance
就沒有達到lazy loading
的效果。 - 結論:這種單利模式可用,可能造成內存浪費。
3. 餓漢式(靜態代碼塊)實現單例
3.1 代碼示例
class Singleton {
// 1. 構造器私有化, 外部能new
private Singleton () {}
// 2. 內部可以創建對象實例
private static Singleton instance;
// 在靜態代碼塊中,創建單利對象
static {
instance = new Singleton();
}
// 3. 提供一個靜態方法,返回實例對象
public static Singleton getInstance() {
return instance;
}
}
3.2 優缺點說明
- 這種方式和上面的實現方式其實類似,只不過將類實例化的過程放在靜態代碼快總,也是在類裝載的時候,就執行靜態代碼塊中的代碼,初始化類的實例。優缺點和餓漢式的靜態常量的一樣。
- 結論:這種單利模式可用,可能造成內存浪費。
4. 懶漢式(線程不安全)實現單例
4.1 代碼示例:
public class Singleton {
private static Singleton instance;
private Singleton () {}
// 提供一個靜態共有的方法,當時用該方法時,纔去創建instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4.2 優缺點說明:
- 起到了
Lazy Loading
的效果,但是只能在單線程下使用。 - 如果在多線程下,一個線程進入
if (singleton == null)
語句塊,還未來的及往下執行,另一個線程也通過了這條判斷語句,這是遍會產生多個實例,所以在多線程環境下不可使用這種方式。 - 結論: 再實際開發中,不可以使用這種方式。
5. 懶漢式(線程安全,同步方法)實現單例
5.1 代碼示例
public class Singleton {
private static Singleton singleton;
private Singleton() {}
// 加入同步處理代碼
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
5.2 優缺點說明:
- 解決了線程不安全問題
- 效率太低,每一個線程在想獲得類的實例的時候,執行
getInstance
方法都要執行通過,而其實這個方法只執行一次實例化代碼就可以,後面想獲得該實例直接return
就可以了,方法進行同步的效率太低了。 - 結論: 在實際開發中,不推薦使用這種方式。
6 懶漢式(線程安全,同步代碼塊)實現單例
6.1 代碼示例
public class Singleton {
private static Singleton singleton;
private Singleton() {}
// 加入同步處理代碼
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
6.2 優缺點說明:
- 這種方式,本意是想對第四種實現方式的改進,因爲前面同步方法效率太低,
改爲同步產生實例化的的代碼塊
- 但是這種同步並不能起到線程同步的作用。跟第3種實現方式遇到的情形一
致,假如一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,
另一個線程也通過了這個判斷語句,這時便會產生多個實例
- 結論:在實際開發中,不能使用這種方式
7. 雙重檢查實現單例
7.1 示例代碼
public class Singleton {
// volatile 禁止重排序,
private static volatile Singleton singleton;
private Singleton() {}
// 重排問題
public static Singleton getInstance () {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton == new Singleton();
}
}
}
return singleton;
}
}
7.2 優缺點說明:
Double-Check
概念是多線程開發中常使用到的,如代碼中所示,我們進行了兩
次if (singleton == null)
檢查,這樣就可以保證線程安全了。
- 這樣,實例化代碼只用執行一次,後面再次訪問時,判斷
if (singleton == null)
,
直接return
實例化對象,也避免的反覆進行方法同步。
-
線程安全;延遲加載;效率較高 。
-
結論:在實際開發中,推薦使用這種單例設計模式。
7.3 指令重排對單例造成的影響
7.3.1 什麼是指令重排
比如 java 中簡單的一句 instance = new Singleton,會被編譯器編譯成如下 JVM 指令:
memory =allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance =memory; //3:設置instance指向剛分配的內存地址
但是這些指令順序並非一成不變,有可能會經過 JVM 和 CPU 的優化,指令重排成下面的順序:
memory =allocate(); //1:分配對象的內存空間
instance =memory; //3:設置instance指向剛分配的內存地址
ctorInstance(memory); //2:初始化對象
7.3.2 影響
對應到上文的單例模式,會產生:
- 當線程 A 執行完1,3,時,準備走2,即 instance 對象還未完成初始化,但已經不再指向 null 。
- 此時如果線程 B 搶佔到CPU資源,執行 if(instance == null)的結果會是 false,
- 從而返回一個沒有初始化完成的instance對象。
7.3.3 解決
// volatile 禁止重排序,
private static volatile Singleton singleton;
很簡單,volatile
修飾符在此處的作用就是阻止變量訪問前後的指令重排,從而保證了指令的執行順序。
意思就是,指令的執行順序是嚴格按照上文的 1、2、3 來執行的,從而對象不會出現中間態。
其實,volatile
關鍵字在多線程的開發中應用很廣,暫不贅述。
8. 靜態內部類實現單例
8.1 示例代碼
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
8.2 優缺點說明
-
這種方式採用了類裝載的機制來保證初始化實例時只有一個線程。
-
靜態內部類方式在
Singleton
類被裝載時並不會立即實例化,而是在需要實例化
時,調用getInstance
方法,纔會裝載SingletonInstance
類,從而完成Singleton
的
實例化。
- 類的靜態屬性只會在第一次加載類的時候初始化,所以在這裏,
JVM
幫助我們
保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。
-
優點:避免了線程不安全,利用靜態內部類特點實現延遲加載,效率高 。
-
結論:推薦使用。
9. 以上實現會遇到的問題
上面的實現方法都包含了一個 private 的構造函數。因此,這是不是意味着,我們就能保證無法創建多個類的實例了呢?
答案是否定的,即我們仍然有其他的高階方法來創建多個類的實施,以破解單例模式。
- 序列化(serialization)/ 反序列化(deserializations)
- 反射(reflection)
9.1 序列化(SERIALIZATION
)/ 反序列化(DESERIALIZATIONS
)問題
序列化可能會破壞單例模式。
通過比較一個序列化後的對象實例和其被反序列化後的對象實例,我們發現他們不是同一個對象,換句話說,在反序列化時會創建一個新的實例(即使定義構造函數爲 private)。
9.1.1 解決方案
在反序列化過程中,如果被序列化的類中定義了 readResolve 方法,虛擬機會試圖調用對象類裏的 readResolve 方法,以進行用戶自定義的反序列化。
最終,實現了在序列化 / 反序列化過程中也不破壞單例模式。
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
//反序列時直接返回當前INSTANCE
private Object readResolve() {
return INSTANCE;
}
}
9.2 反射(REFLECTION)
類似地,使用反射(reflection)可以強行調用私有構造器。
9.2.1 解決
public static Singleton INSTANCE = new Singleton();
private static volatile boolean flag = true;
private Singleton(){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists !");
}
}
10 枚舉實現單例
10.1 示例代碼
public enum SingletonEnum {
INSTANCE;
int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
10.2 優缺點說明
優缺點說明:
- 這藉助
JDK1.5
中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而
且還能防止反序列化重新創建新的對象。
- 這種方式是
Effective Java
作者Josh Bloch
提倡的方式 - 使用枚舉的方法是起到了單例的作用,但是也有一個弊端,那就是 無法進行懶加載。
- 結論:推薦使用
11. 單例模式注意事項和細節說明
- 單例模式保證了 系統內存中該類只存在一個對象,節要頻繁創建銷燬的對象,使用單例模式可以提高系統
- 當想實例化一個單例類的時候,必須要記住使用相應用new
- 單例模式使用的場景:需要頻繁的進行創建和銷燬的耗費資源過多(即:重量級對象),但又經常用到的對據庫或文件的對象(比如數據源、session工廠等)