單例模式-最簡單的設計模式?
一.說在前面
在系統開發設計中,總會存在這麼幾種情況,①需要頻繁建立銷燬的物件,②建立物件需要消耗很多資源,但又經常用到的物件(如工具類物件,頻繁訪問資料庫或檔案的物件,資料來源,session工廠等);③某個類只能有一個物件,如應用中的Application類;這時就應該考慮使用單例模式。個人部落格地址www.mycookies.cn
二.單例模式的動機
- 在軟體系統中,經常有一些特殊的類,必須保證他們在系統中只存在一個例項,才能保證他們的邏輯正確性,以及良好的效率
- 如何繞過常規的構造器,提供以中機制保證一個類只有一個例項?這應該是類設計的責任,而不是使用者的責任
三.模式定義
確保類只有一個例項,並提供全域性訪問點。
餓漢式
在類初始化完成之後就完成了物件建立, 無論是否使用都要提前進行例項化。
/** * 餓漢式[工廠方法] * * @author Jann Lee * @date 2019-07-21 14:28 */ public class Singleton1 { private static final Singleton1 instance = new Singleton1(); private Singleton1() { } public static Singleton1 getInstance() { return instance; } } /** * 餓漢式[公有域] */ public class Singleton2 { public static final Singleton2 instance = new Singleton2(); private Singleton2() { } } /** * 餓漢式[靜態程式碼塊,工廠方法] */ public class Singleton3 { private static Singleton3 instance; static { instance = new Singleton3(); } private Singleton3() { } public static Singleton3 getInstance() { return instance; } }
以上三種寫法僅僅是在程式碼實現上的差異,是“餓漢式”最常見的實現。
優點:類裝在時候完成例項化,避免了多執行緒問題
缺點:可能造成記憶體浪費(可能例項從來沒有被使用到)
懶漢式
顧名思義,因為懶,所以在用到時才會進行例項化。
實現思路:私有化構造方法-> 宣告成員變數 -> 提供公共方法訪問【如果成員變數不為空,直接返回,如果為空建立後返回】
/** * 1.懶漢式[非執行緒安全] * * @author Jann Lee * @date 2019-07-21 14:31 **/ public class Singleton1 { private static Singleton1 instance; private Singleton1() { } public static Singleton1 getInstance() { if (instance == null) { instance = new Singleton1(); } return instance; } }
上述實現方式是最容易被想到的,但是應該也算是一種錯誤的實現方式,因為再多執行緒環境下一個物件可能被建立了多次。
為了解決執行緒安全問題, 將方法進行同步
/**
* 2.懶漢式[同步方法]
**/
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {
}
/**
* 同步方法,保證執行緒安全
*/
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
的確,這樣實現解決了併發情況下帶來的問題。但是每次獲取物件都需要進行同步,然而物件建立只需要進行一次即可;因為併發讀取資料不需要進行同步,所以這種在方法進行同步是沒有必要的,在併發情況下肯定會帶來效能上的損失。
這時我們首先想到的時縮小同步範圍,只有在建立物件的時候使用同步,即使用同步程式碼塊實現。
/**
* 4.懶漢式[同步程式碼塊]
**/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
/**
* 同步程式碼塊,並不能保證執行緒安全
*/
public static Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) {
instance = new Singleton3();
}
}
return instance;
}
}
這是一種錯誤的實現方式,雖然減少了加鎖範圍,但是又回到了併發環境下的重複建立物件的問題,具體分析思路可以參考上文中的圖。兩個執行緒在if (instance==null)之後發生了競爭,執行緒1獲取到了鎖,執行緒2掛起,執行完畢後釋放鎖,此時執行緒二獲取到鎖後回接著往下執行,再次建立物件。
解決上述問題,我們只需要在同步程式碼塊中加入校驗。
/**
* 懶漢式[雙重檢查鎖]
**/
public class Singleton4 {
private static volatile Singleton4 instance;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
注意:此處除了再次校驗是否為空之外,還給成員變數之前加上了一個volatile關鍵字,這個非常重要。因為在程式碼執行過程中會發生指令重排序。這裡你只需要知道加上volatile之後能保證指令不會被重排序,程式能正確執行,而不加則可能不出錯。
在編譯器和處理器為了提高程式的執行效能,會對指令進行重新排序。即程式碼會被編譯成操作指令,而指令執行順序可能會發生變化。
java中建立物件分為 三個步驟【可以簡單理解為三條指令】
- 分配記憶體,記憶體空間初始化
- 物件初始化,類的元資料資訊,hashCode等資訊
- 將記憶體地址返回
如果2,3順序發生了變化,另一個執行緒獲得鎖時恰好還沒有完成物件初始化,即instance指向null,就會重複建立物件。
靜態內部類
在靜態內部類中持有一個物件。
/**
* 使用靜態內部類實現單例模式
*
* @author Jann Lee
* @date 2019-07-21 14:47
**/
public class Singleton {
private Singleton (){}
public static Singleton getInstance() {
return ClassHolder.singleton;
}
/**
* Singleton裝載完成後,不會建立物件
* 呼叫getInstance時候,靜態內部類ClassHolder才進行裝載
*/
private static class ClassHolder {
private static final Singleton singleton = new Singleton();
}
}
靜態內部類的實現則是根據java語言特性實現的,即讓靜態內部類持有一個物件;根據類載入機機制的特點,每個類只會載入一次,並且只有呼叫getInstance方法時才會載入內部類。這樣就能保證物件只被建立一次,無論是否在多執行緒環境中。
只包含單個元素的列舉型別
在java中列舉也是類的一種,實際上列舉的每一個元素都是一個列舉類的物件,可以理解為對類的一種封裝,預設私有構造方法,且不能使用public修飾。
/**
* 列舉實現單例模式
*
* 為了便於理解給列舉類添加了兩個屬性
* @author Jann Lee
* @date 2019-07-21 14:55
**/
public enum Singleton {
INSTANCE;
private String name;
private int age;
Singleton04() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
四.要點總結
- SIngleton模式中的例項構造器可以設定為protected以允許字類派生
- Singleton模式一般不要支援拷貝建構函式和clone介面,因為這有可能導致多個物件例項,與Singleton模式的初衷違背。
- 如何實現多執行緒環境下安全的Singleton,注意對雙重檢查鎖的正確實現。
五.思考
- java中建立物件的方式有很多種,其中當然包括反射,反序列化,那麼上述各種設計模式還能保證物件只會被建立一次嗎?(這個問題會在下一篇 中進行分析)
- volatile關鍵字是一個非常重要的關鍵字,它有那些功能?