Java設計模式(五):單例設計模式
1. 應用場景
一個無狀態的類使用單例模式節省記憶體資源。 比如說執行緒池、快取、對話方塊、設定偏好和登錄檔物件、日誌物件、充當印表機、顯示卡等裝置的驅動程式物件。
2. 概念
確保一個類只有一個例項,並提供該例項的全域性訪問點。
3.Class Diagram
使用一個私有建構函式、一個私有靜態變數以及一個公有靜態函式來實現。
私有建構函式保證了不能通過建構函式來建立物件例項,只能通過公有靜態函式返回唯一的私有靜態變數。
4. Implementation
4.1 懶漢式-執行緒不安全
以下實現中,私有靜態變數 uniqueInstance 被延遲例項化,這樣做的好處是,如果沒有用到該類,那麼就不會例項化 uniqueInstance,從而節約資源。
這個實現在多執行緒環境下是不安全的,如果多個執行緒能夠同時進入 if (uniqueInstance == null) ,並且此時 uniqueInstance 為 null,那麼會有多個執行緒執行 uniqueInstance = new Singleton(); 語句,這將導致例項化多次 uniqueInstance。
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
System.out.println("Creating unique instance of Chocolate Boiler");
uniqueInstance = new ChocolateBoiler();
}
System.out.println("Returning instance of Chocolate Boiler" );
return uniqueInstance;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
// fill the boiler with a milk/chocolate mixture
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
// drain the boiled milk and chocolate
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
// bring the contents to a boil
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
4.2 餓漢式-執行緒安全
執行緒不安全問題主要是由於 uniqueInstance 被例項化多次,採取直接例項化 uniqueInstance 的方式就不會產生執行緒不安全問題。
但是直接例項化的方式也丟失了延遲例項化帶來的節約資源的好處。
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance =new ChocolateBoiler();
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public ChocolateBoiler getInstance() {
return uniqueInstance;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
// fill the boiler with a milk/chocolate mixture
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
// drain the boiled milk and chocolate
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
// bring the contents to a boil
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
4.3 懶漢式-執行緒安全
只需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點只能有一個執行緒能夠進入該方法,從而避免了例項化多次 uniqueInstance。
但是當一個執行緒進入該方法之後,其它試圖進入該方法的執行緒都必須等待,即使 uniqueInstance 已經被例項化了。這會讓執行緒阻塞時間過長,因此該方法有效能問題,不推薦使用。
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public synchronized static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
System.out.println("Creating unique instance of Chocolate Boiler");
uniqueInstance = new ChocolateBoiler();
}
System.out.println("Returning instance of Chocolate Boiler");
return uniqueInstance;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
// fill the boiler with a milk/chocolate mixture
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
// drain the boiled milk and chocolate
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
// bring the contents to a boil
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
4.4 雙重校驗鎖-執行緒安全
uniqueInstance 只需要被例項化一次,之後就可以直接使用了。加鎖操作只需要對例項化那部分的程式碼進行,只有當 uniqueInstance 沒有被例項化時,才需要進行加鎖。
雙重校驗鎖先判斷 uniqueInstance 是否已經被例項化,如果沒有被例項化,那麼才對例項化語句進行加鎖。
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static volatile ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
synchronized (ChocolateBoiler.class){
if(uniqueInstance==null){
System.out.println("Creating unique instance of Chocolate Boiler");
uniqueInstance = new ChocolateBoiler();
}
}
}
System.out.println("Returning instance of Chocolate Boiler");
return uniqueInstance;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
// fill the boiler with a milk/chocolate mixture
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
// drain the boiled milk and chocolate
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
// bring the contents to a boil
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
考慮下面的實現,也就是隻使用了一個 if 語句。在 uniqueInstance == null 的情況下,如果兩個執行緒都執行了 if 語句,那麼兩個執行緒都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個執行緒都會執行 uniqueInstance = new Singleton(); 這條語句,只是先後的問題,那麼就會進行兩次例項化。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句。
if (uniqueInstance == null) {
synchronized (Singleton.class) {
uniqueInstance = new ChocolateBoiler();
}
}
uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段程式碼其實是分為三步執行:
- 為 uniqueInstance 分配記憶體空間
- 初始化 uniqueInstance
- 將 uniqueInstance 指向分配的記憶體地址
但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1>3>2。指令重排在單執行緒環境下不會出現問題,但是在多執行緒環境下會導致一個執行緒獲得還沒有初始化的例項。例如,執行緒 T1 執行了 1 和 3,此時 T2 呼叫 getUniqueInstance() 後發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常執行。
4.5 靜態內部類實現
當 Singleton 類載入時,靜態內部類 SingletonHolder 沒有被載入進記憶體。只有當呼叫 getUniqueInstance() 方法從而觸發 SingletonHolder.INSTANCE 時 SingletonHolder 才會被載入,此時初始化 INSTANCE 例項,並且 JVM 能確保 INSTANCE 只被例項化一次。
這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對執行緒安全的支援。
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static class ChocolateBoilerHolder{
private static ChocolateBoiler uniqueInstance=new ChocolateBoiler();
}
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
return ChocolateBoilerHolder.uniqueInstance;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
// fill the boiler with a milk/chocolate mixture
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
// drain the boiled milk and chocolate
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
// bring the contents to a boil
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
4.6 列舉實現
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 單例測試
Singleton firstSingleton = Singleton.INSTANCE;
firstSingleton.setObjName("firstName");
System.out.println(firstSingleton.getObjName());
Singleton secondSingleton = Singleton.INSTANCE;
secondSingleton.setObjName("secondName");
System.out.println(firstSingleton.getObjName());
System.out.println(secondSingleton.getObjName());
// 反射獲取例項測試
try {
Singleton[] enumConstants = Singleton.class.getEnumConstants();
for (Singleton enumConstant : enumConstants) {
System.out.println(enumConstant.getObjName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
該實現在多次序列化再進行反序列化之後,不會得到多個例項。而其它實現需要使用 transient 修飾所有欄位,並且實現序列化和反序列化的方法。
該實現可以防止反射攻擊。在其它實現中,通過 setAccessible() 方法可以將私有建構函式的訪問級別設定為 public,然後呼叫建構函式從而例項化物件,如果要防止這種攻擊,需要在建構函式中新增防止多次例項化的程式碼。該實現是由 JVM 保證只會例項化一次,因此不會出現上述的反射攻擊。
5.Examples
- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons