1. 程式人生 > >Java設計模式(五):單例設計模式

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(); 這段程式碼其實是分為三步執行:

  1. 為 uniqueInstance 分配記憶體空間
  2. 初始化 uniqueInstance
  3. 將 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

6. JDK