通過單例模式可以保證系統中一個類只有一個實體而且該實體易于被外界訪問,從而方便對實體個數的控制并節約系統資源,如果希望在系統中某個類的物件只能存在一個,單例模式是最好的解決辦法,
模式動機與定義
對系統中某些類來說,只有一個實體很重要,例如,一個系統只能有一個視窗管理器或檔案系統,因此確保系統中某個物件的唯一性即一個類只能有一個實體很重要,
單例模式確保某一個類只有一個實體,而且自行實體化并向整個系統提供這個實體,這個類稱為單例類,它提供全域訪問方法,單例模式有三個要點:一是某個類只能有一個實體;二是它必須自行創建這個實體;三是它必須自行向整個系統提供這個實體,
模式分析
單例模式的目的是保證類僅有一個實體,并提供一個訪問它的全域訪問點,單例類擁有一個私有建構式,確保用戶無法通過new關鍵字直接實體化,除此之外,該模式包含一個靜態私有成員變數與靜態公有的工廠方法,該工廠方法負責校驗實體存在性并實體化自己,然后存盤在靜態成員變數中,以確保只有一個實體被創建,因此在單例模式的實作程序中要注意三點:
- 單例類的建構式為私有
- 提供一個自身的靜態私有成員變數
- 提供一個公有的靜態工廠方法
public class Singleton {
private static Singleton instance = null;
//私有構造方法
private Singleton() {}
//靜態公有工廠方法,回傳唯一實體
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Client {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true
}
}
模式優缺點
單例模式優點如下:
- 提供對外唯一實體的受控訪問
- 由于在系統記憶體中只存在一個物件,因此可以節約系統資源
- 允許可變數目的實體,基于單例模式我們可以進行擴展,使用與單例模式相似的方法來獲得指定個數的物件實體
單例模式缺點如下:
- 由于單例模式中沒有抽象層,因此單例類的擴展有很大困難
- 單例類的職責過重,一定程度上違背了單一職責原則
- 如果實體化的物件長時間不被利用,系統會認為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實體化,將導致物件狀態的丟失
模式適用環境
在以下情況可以適用單例模式:
- 系統只需要一個實體物件,或者需要考慮資源消耗太大而只允許創建一個物件
- 客戶呼叫類的單個實體只允許一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實體
使用單例模式有一個必要條件:在一個系統中要求一個類只有一個實體時才應當使用單例模式,反過來,如果一個類可以有幾個實體共存,就需要對單例模式進行改進,使之成為多例模式,在使用程序中我們還需注意以下兩個問題:
- 不要使用單例模式存取全域變數,因為這違背了單例模式的用意
- 不要將資料庫連接做成單例,因為一個系統可能會與資料庫有多個連接,否則可能造成資源無法及時釋放
餓漢式單例類和懶漢式單例類
在定義靜態變數的時候實體化單例類,在類加載的時候就已經創建了單例物件,在這個類被加載時,靜態變數 instance 會被初始化,此時類的私有建構式會被呼叫,單例類的唯一實體將被創建,
public class EagerSingleton {
//可以由名知意,太饑餓了,一大早就準備好了實體等待使用,但很有可能一直沒人使用
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
懶漢式單例類不是在定義靜態變數時實體化單例類,而是在呼叫靜態工廠方法時實體化單例類,因此在類加載時并沒有創建單例物件,
public class LazySingleton {
//等到人家使用才生產一個實體,所以叫懶漢模式
private static LazySingleton instance = null;
private LazySingleton() {}
public static LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
從資源利用效率角度來說,懶漢式單例類比餓漢式單例類稍好,從速度和反應時間角度來說,餓漢式單例類比懶漢式稍好,然而,懶漢式單例類在實體化時,必須處理好多個執行緒同時首次參考此類時的訪問限制問題,就上述的懶漢式單例類實體來說,是執行緒不安全的,為了實作執行緒安全,必須加鎖 synchronized 實作單例,但加鎖會影響效率,
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
雙重校驗鎖懶漢模式(DCL)
由于 synchronized 具有排他性,同一時刻只有一個執行緒進入到同步代碼塊或者同步方法之中,每次只能通過一個執行緒,效率自然很低,采用雙鎖機制,在保證安全的前提下還能保持高性能,
public class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() {}
public static LazySingleton getInstance() {
// 由于單例模式只需創建一次實體即可
// 所有當已經創建一個實體之后,再次呼叫就不需要進入同步代碼塊中了
// 避免執行緒之間互相競爭鎖,提高了性能
if(instance == null) {
synchronized(LazySingleton.class) {
// 防止二次創建實體
if(instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
其中變數 instance 還使用了 volatile 修飾,使用 volatile 的目的是防止指令重排序以及保證變數在多執行緒運行時的可見性,
-
指令重排序
指令是計算機執行某種操作的命令,一個執行緒內部的指令并不一定是按照順序執行的,為了性能優化,在不改變程式執行結果的前提下,重新安排陳述句的執行順序,這些重排序會導致執行緒問題,如 instance = new LazySingleton() 這句話可以分為三步:
- 為 instance 分配記憶體空間
- 初始化 instance
- 將 singleton 指向分配的記憶體空間
由于執行重排序,執行順序可能變成 1-3-2,指令重排在單執行緒下不會出現問題,但是在多執行緒下會導致一個執行緒獲得一個未初始化的實體,例如:執行緒 T1 執行了 1 和 3,此時 T2 呼叫 getInstance() 后發現 instance 不為空,因此回傳 instance , 但是此時的 singleton 還沒有被初始化,
-
保證變數在多執行緒運行時的可見性
各個執行緒在作業時,會將共享變數從主記憶體中拷貝到本地的作業記憶體,然后基于作業記憶體中的資料進行操作處理,在修改完之后的某一個時刻(執行緒退出之前),自動把修改過后副本的值回寫到主存,這就可能造成一個執行緒在主存中修改了變數的值,而另外一個執行緒還繼續使用它在本地作業記憶體的變數值的拷貝,造成資料的不一致,
使用 volatile 關鍵字,強制執行緒每次讀取變數的值的時候都必須去主記憶體中取值,從而避免資料臟讀的現象,保證每個執行緒都能獲得該變數的最新值,
登記式/靜態內部類模式
這種方法不僅能確保執行緒安全,也能保證單例的唯一性,同時也延遲了單例的實體化,
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
private static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
內部類(不論是靜態內部類還是非靜態內部類)都是在第一次使用時才會被加載,而且靜態內部類的加載不需要依附外部類,在使用時才加載,因此只有當呼叫 SingletonHolder.INSTANCE 這條陳述句時才會初始化 INSTANCE,
那么會不會出現多個執行緒同時加載 SingletonHolder 類的情況呢?答案是不會,JVM 規定,如多個執行緒用到同一個類,而這個類還未被加載,則只有一個執行緒去加載類,其他執行緒等待,因此靜態內部類模式的單例可以保證執行緒安全,
列舉模式
列舉天然解決了多執行緒同步執行的問題,而且還自動支持序列化機制,防止反序列化重新創建新的物件,絕對防止多次實體化,
public enum Singleton {
INSTANCE;
public void method() {}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/183089.html
標籤:其他
