單例模式&有上限多例模式

單例模式(Singleton Pattern) 創建型模式,範疇:對象

定義

確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

單例模式的通用類圖

在這裏插入圖片描述
Singleton類稱爲單例類,通過使用private的構造函數確保了在一個應用中只產生一個實例,並且是自行實例化的
通用代碼:

package com.lushunde.desingn.singleton;

/**
 * 通用模式-餓漢式
 * @author bellus
 *  */
public class Singleton {
	private static final Singleton singleton = new Singleton();

	// 限制產生多個對象
	private Singleton() {
	}

	// 通過該方法獲得實例對象
	public static Singleton getSingleton() {
		return singleton;
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}
}

實現單例的幾種形式

實現方式 優點 缺點 效率
餓漢式(通用形式) 線程安全,效率極高 缺少延時加載
懶漢式(方法同步) 線程安全,能延時加載 效率不高(每次加鎖)
雙重檢測鎖式(volatile) 線程安全,效率很高,能延時加載
靜態內部類 線程安全,效率高,能延時加載
枚舉單例 線程安全,效率高 ,能防止反序列化 缺少延時加載

餓漢式

天然線程安全,無懶加載,效率高


package com.lushunde.desingn.singleton.improve;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 通用模式-餓漢式,解決注意事項
 *  天然線程安全,無懶加載,效率高
 * @author bellus
 *
 */
public class Singleton implements Serializable {
	private static final long serialVersionUID = -7640246007296163797L;
	
	private static final Singleton singleton = new Singleton();

	// 限制產生多個對象
	private Singleton()  {
		//解決反射創建對象,直接拋異常
		if(singleton!=null){
			throw new RuntimeException("防止反射創建對象");
		}
	}
	
	// 通過該方法獲得實例對象
	public static Singleton getSingleton() {
		return singleton;
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}
	
	//解決反序列化漏洞
	public Object readResolve() throws ObjectStreamException{
		return singleton;
	}
}

餓漢式(加鎖)

每次獲取都要加鎖,影響效率

package com.lushunde.desingn.singleton.improve;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 通用模式-懶漢式,解決線程安全、反射漏洞、反序列化
 * 
 * @author bellus
 *
 */
public class Singleton2 implements Serializable {

	private static final long serialVersionUID = -7640246007296163797L;
	
	private static Singleton2 singleton = null;

	// 限制產生多個對象
	private Singleton2()  {
		//解決反射創建對象,直接拋異常
		if(singleton!=null){
			throw new RuntimeException("防止反射創建對象");
		}
	}

	// 通過該方法獲得實例對象+ 加鎖解決線程安全問題
	public static synchronized Singleton2  getSingleton() {
		if(singleton==null){
			singleton = new Singleton2();
		}
		return singleton;
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}
	
	//解決反序列化漏洞
	public Object readResolve() throws ObjectStreamException{
		return singleton;
	}
}

雙重檢查鎖

DCL失效問題,通過volatile可以解決。

INSTANCE = new SingleTon();
這個步驟,其實在jvm裏面的執行分爲三步:
1.在堆內存開闢內存空間。
2.在堆內存中實例化SingleTon裏面的各個參數。
3.把對象指向堆內存空間。
由於jvm存在亂序執行功能,所以可能在2還沒執行時就先執行了3,如果此時再被切換到線程B上,由於執行了3,INSTANCE 已經非空了,會被直接拿出來用,這樣的話,就會出現異常。這個就是著名的DCL失效問題。
不過在JDK1.5之後,官方也發現了這個問題,故而具體化了volatile,即在JDK1.6及以後,只要定義爲private volatile static SingleTon INSTANCE = null;就可解決DCL失效問題。volatile確保INSTANCE每次均在主內存中讀取,這樣雖然會犧牲一點效率,但也無傷大雅。

package com.lushunde.desingn.singleton.improve;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 雙重加鎖,解決線程安全、效率、反射漏洞、反序列化
 * 
 * INSTANCE  = new SingleTon(); 
 * 這個步驟,其實在jvm裏面的執行分爲三步:
 *      1.在堆內存開闢內存空間。
 *      2.在堆內存中實例化SingleTon裏面的各個參數。
 *      3.把對象指向堆內存空間。
 * 由於jvm存在亂序執行功能,所以可能在2還沒執行時就先執行了3,如果此時再被切換到線程B上,由於執行了3,INSTANCE 已經非空了,會被直接拿出來用,這樣的話,就會出現異常。這個就是著名的DCL失效問題。
 * 不過在JDK1.5之後,官方也發現了這個問題,故而具體化了volatile,即在JDK1.6及以後,只要定義爲private volatile static SingleTon  INSTANCE = null;就可解決DCL失效問題。volatile確保INSTANCE每次均在主內存中讀取,這樣雖然會犧牲一點效率,但也無傷大雅。
 * @author bellus
 *
 */
public class Singleton3 implements Serializable {

	private static final long serialVersionUID = -7640246007296163797L;

	//volatile 解決DCL失效
	private static volatile Singleton3 singleton = null;

	// 限制產生多個對象
	private Singleton3() {
		// 解決反射創建對象,直接拋異常
		if (singleton != null) {
			throw new RuntimeException("防止反射創建對象");
		}
	}

	// 通過該方法獲得實例對象+ 加鎖解決線程安全問題,雙重判斷 提高效率
	public static  Singleton3 getSingleton() {
		if (singleton == null) {
			synchronized (Singleton3.class) {
				if (singleton == null) {
					singleton = new Singleton3();
				}
			}
		}
		return singleton;
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}

	// 解決反序列化漏洞
	public Object readResolve() throws ObjectStreamException {
		return singleton;
	}
}

靜態內部類

通過類加載機制實現懶加載和單例

package com.lushunde.desingn.singleton.improve;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 靜態內部類 
 * 外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不佔內存
 * 
 * @author bellus
 *
 */
public class Singleton4 implements Serializable {

	private static final long serialVersionUID = -7640246007296163797L;

	private Singleton4() {
		// 解決反射創建對象,直接拋異常
		if (SingletonHoler.INSTANCE != null) {
			throw new RuntimeException("防止反射創建對象");
		}
	}

	private static class SingletonHoler {
		private static Singleton4 INSTANCE = new Singleton4();
	}

	public static Singleton4 getSingleton() {
		return SingletonHoler.INSTANCE;
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}

	// 解決反序列化漏洞
	public Object readResolve() throws ObjectStreamException {
		return SingletonHoler.INSTANCE;
	}

}

枚舉

天然的 線程安全、無反射漏洞,無序列化漏洞

package com.lushunde.desingn.singleton.improve;

import java.io.Serializable;

/**
 * 枚舉 
 * 默認就  線程安全、無反射漏洞,無序列化漏洞
 * @author bellus
 *
 */
public enum Singleton5 implements Serializable {

	INSTANCE;

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}

}

擴展:有上限的多例模式

含義:
產生固定數量對象的模式就叫做有上限的多例模式,它是單例模式的一種擴展,採用有上限的多例模式,我們可以在設計時決定在內存中有多少個實例,方便系統進行擴展,修正單例可能存在的性能問題,提供系統的響應速度。

常用場景

  • 讀取文件,設置多個固定提高併發量,繼而提升性能和響應速度。

  • 有上限的多例模式代碼:
    MultitonSingleton 代碼:

package com.lushunde.desingn.singleton.improve;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Random;

/**
 * 
 * 擴展:有上限的多例模式 **含義:**
 * 產生固定數量對象的模式就叫做有上限的多例模式,它是單例模式的一種擴展,採用有上限的多例模式,我們可以在設計時決定在內存中有多少個實例,方便系統進行擴展,
 * 修正單例可能存在的性能問題,提供系統的響應速度。 **常用場景**: 讀取文件,設置多個固定提高併發量,繼而提升性能和響應速度。
 * 
 * @author bellus
 *
 */
public class MultitonSingleton implements Serializable {

	private static final long serialVersionUID = -7640246007296163797L;

	// 定義最多能產生的實例的數量
	private static int maxNumOfMultiton = 2;

	// 定義一個列表,容納所有實例
	private static ArrayList<MultitonSingleton> SingletonList = new ArrayList<MultitonSingleton>();
	// 當前實例的序列號
	private static int countNumOfMultiton = 0;

	// 產生所有對象
	static {
		for (int i = 0; i < maxNumOfMultiton; i++) {
			SingletonList.add(new MultitonSingleton());
		}
	}

	private MultitonSingleton() {
		// 解決反射創建對象,直接拋異常
		if (SingletonList.size() > maxNumOfMultiton) {
			throw new RuntimeException("創建數已達到最大數,防止反射創建對象");
		}
	}

	// 隨機獲得一個皇帝對象
	public static MultitonSingleton getInstance() {
		Random random = new Random();
		countNumOfMultiton = random.nextInt(maxNumOfMultiton);
		return SingletonList.get(countNumOfMultiton);
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {

	}

	// 解決反序列化漏洞
	public Object readResolve() throws ObjectStreamException {
		return SingletonList.get(countNumOfMultiton);
	}

}

MultitonClient6 代碼:

package com.lushunde.desingn.singleton.improve;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;

/**
 * 有上限的多例模式 測試 正常調用 反射漏洞 反序列化漏洞
 * 
 * @author bellus
 *
 */

public class MultitonClient6 {

	public static void main(String[] args) {

		MultitonSingleton instance = MultitonSingleton.getInstance();

		// 多次調用,查看有多少對象
		test1();
		// 實現反射創建新對象
		testReflect(instance);
		// 通過反序列化
		testSerzi(instance);

	}

	private static void test1() {
		// 調用10次,查看對象
		MultitonSingleton instance = null;
		for (int i = 0; i < 10; i++) {
			instance = MultitonSingleton.getInstance();
			System.out.println(instance);
		}

	}

	private static void testReflect(MultitonSingleton instance) {
		try {

			// 實現反射創建新對象
			Class<?> classType = Singleton5.class;
			Class<?>[] cArg = new Class[0]; // 入參類型(空參)
			// 獲取 構造方法 類
			Constructor<?> constructor = classType.getDeclaredConstructor(cArg);
			// 打開 私有方法開關
			constructor.setAccessible(true);
			// 創建新對象
			MultitonSingleton instance3 = (MultitonSingleton) constructor.newInstance();

			System.out.print("反射創建: ");
			if (instance == instance3) {
				System.out.println("同一個對象");
			} else {
				System.out.println("不同對象");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static void testSerzi(MultitonSingleton instance) {
		// 通過反序列化獲取對象和一中其中一個相同

		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./singleton.txt"));
				ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./singleton.txt"))) {

			oos.writeObject(instance);
			oos.close();
			MultitonSingleton instance4 = (MultitonSingleton) ois.readObject();

			ois.close();
			System.out.println("反序列化獲取對象:" + instance4);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

單例模式的優點

  • 由於單例模式在內存中只有一個實例,減少了內存開支,特別是一個對象需要頻繁地創建、銷燬時,而且創建或銷燬時性能又無法優化,單例模式的優勢就非常明顯。
  • 由於單例模式只生成一個實例,所以減少了系統的性能開銷,當一個對象的產生需要比較多的資源時,如讀取配置、產生其他依賴對象時,則可以通過在應用啓動時直接產生一個單例對象,然後用永久駐留內存的方式來解決(在Java EE中採用單例模式時需要注意JVM垃圾回收機制)。
  • 單例模式可以避免對資源的多重佔用,例如一個寫文件動作,由於只有一個實例存在內存中,避免對同一個資源文件的同時寫操作。
  • 單例模式可以在系統設置全局的訪問點,優化和共享資源訪問,例如可以設計一個單例類,負責所有數據表的映射處理。

單例模式的缺點

  • 單例模式一般沒有接口,擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑可以實現。單例模式爲什麼不能增加接口呢?因爲接口對單例模式是沒有任何意義的,它要求“自行實例化”,並且提供單一實例、接口或抽象類是不可能被實例化的。當然,在特殊情況下,單例模式可以實現接口、被繼承等,需要在系統開發中根據環境判斷。
  • 單例模式對測試是不利的。在並行開發環境中,如果單例模式沒有完成,是不能進行測試的,沒有接口也不能使用mock的方式虛擬一個對象。
  • 單例模式與單一職責原則有衝突。一個類應該只實現一個邏輯,而不關心它是否是單例的,是不是要單例取決於環境,單例模式把“要單例”和業務邏輯融合在一個類中。

單例模式的使用場景

  • 要求生成唯一序列號的環境
  • 在整個項目中需要一個共享訪問點或共享數據,例如一個Web頁面上的計數器,可以不用把每次刷新都記錄到數據庫中,使用單例模式保持計數器的值,並確保是線程安全的;
  • 創建一個對象需要消耗的資源過多,如要訪問IO和數據庫等資源;
  • 需要定義大量的靜態常量和靜態方法(如工具類)的環境,可以採用單例模式(當然,也可以直接聲明爲static的方式)。

具體場景

  • Spring中 bean對象創建,參數scope中有單例模式(默認,Spring容器可以管理這些Bean的生命期;如果採用非單例模式Prototype類型,則Bean初始化後的管理交由J2EE容器,Spring容器不再跟蹤管理Bean的生命週期)
  • servlet中每個servlet實例(Application servlet)
  • Spring MVC 和 Struts1 框架中,控制器對象實用單例模式
  • 應用程序的日誌應用一般都是用單例模式
  • 項目中讀取配置文件對象
  • 操作系統的文件系統、任務管理器、回收站(windows)
  • 數據庫的連接池(有上限的多例模式)

開發使用場景

  • 封裝工具類(也可以直接static聲明方式)
  • 計數器、生成唯一序列等

單例模式的注意事項

1. 單例模式的線程同步問題

餓漢式存在此問題,需要通過加鎖synchronized實現線程安全

2. 考慮對象的clone複製實例

解決該問題的最好方法就是單例類不要實現Cloneable接口,此時clone方法不能複製

3. 通過反射創建對象

私有構造器加入非空判斷拋異常,這樣反射調用時就會拋異常

	private Singleton()  {
		//解決反射創建對象,直接拋異常
		if(singleton!=null){
			throw new RuntimeException("防止反射創建對象");
		}
	}

4. 通過反序列化創建對象

重寫readresolve()方法,實現反序列化時直接返回已有的實例

	//解決反序列化漏洞
	public Object readResolve() throws ObjectStreamException{
		return singleton;
	}

反射和反序列化漏洞代碼演示

singleton 代碼:

package com.lushunde.desingn.singleton;

import java.io.Serializable;

/**
 * 通用模式-餓漢式
 * 
 * @author bellus
 *
 */
public class Singleton implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = -3273287152119518024L;
	private static final Singleton singleton = new Singleton();

	// 限制產生多個對象
	private Singleton() {
	}

	// 通過該方法獲得實例對象
	public static Singleton getSingleton() {
		return singleton;
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}
}

Client 代碼

package com.lushunde.desingn.singleton;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;

/**
 * 測試 正常調用
 * 反射漏洞
 * 反序列化漏洞
 * @author bellus
 *
 */

public class Client {

	public static void main(String[] args) {

		Singleton singleton = Singleton.getSingleton();

		// 調用兩次
		test1(singleton);
		// 實現反射創建新對象
		testReflect(singleton);
		// 通過反序列化
		testSerzi(singleton);

	}

	private static void test1(Singleton singleton) {
		// 調用兩次,查看是否同一個對象
		Singleton singleton2 = Singleton.getSingleton();
		System.out.print("正常創建: ");
		if (singleton == singleton2) {
			System.out.println("同一個對象");
		} else {
			System.out.println("不同對象");
		}
	}

	private static void testReflect(Singleton singleton) {
		try {

			// 實現反射創建新對象
			Class<?> classType = Singleton.class;
			Class<?>[] cArg = new Class[0]; // 入參類型(空參)
			// 獲取 構造方法 類
			Constructor<?> constructor = classType.getDeclaredConstructor(cArg);
			// 打開 私有方法開關
			constructor.setAccessible(true);
			// 創建新對象
			Singleton singleton3 = (Singleton) constructor.newInstance();

			System.out.print("反射創建: ");
			if (singleton == singleton3) {
				System.out.println("同一個對象");
			} else {
				System.out.println("不同對象");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static void testSerzi(Singleton singleton) {
		// 通過反序列化

		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./singleton.txt"));
				ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./singleton.txt"))) {

			oos.writeObject(singleton);
			oos.close();
			Singleton singleton4 = (Singleton) ois.readObject();

			ois.close();
			System.out.print("反序列化創建: ");
			if (singleton == singleton4) {
				System.out.println("同一個對象");
			} else {
				System.out.println("不同對象");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

結果:

正常創建: 同一個對象
反射創建: 不同對象
反序列化創建: 不同對象

反射和反序列化漏洞修復後代碼演示

只需要修改單例代碼:

package com.lushunde.desingn.singleton.improve;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 通用模式-餓漢式,解決注意事項
 * 
 * @author bellus
 *
 */
public class Singleton implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -7640246007296163797L;
	
	private static final Singleton singleton = new Singleton();

	// 限制產生多個對象
	private Singleton()  {
		//解決反射創建對象,直接拋異常
		if(singleton!=null){
			throw new RuntimeException("防止反射創建對象");
		}
	}

	// 通過該方法獲得實例對象
	public static Singleton getSingleton() {
		return singleton;
	}

	// 類中其他方法,儘量是static
	public static void doSomething() {
	}
	
	
	//解決反序列化漏洞
	public Object readResolve() throws ObjectStreamException{
		return singleton;
	}

	
	
}

再次調用client結果如下:

正常創建: 同一個對象
java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.lushunde.desingn.singleton.improve.Client.testReflect(Client.java:54)
	at com.lushunde.desingn.singleton.improve.Client.main(Client.java:26)
Caused by: java.lang.RuntimeException: 防止反射創建對象
	at com.lushunde.desingn.singleton.improve.Singleton.<init>(Singleton.java:25)
	... 6 more
反序列化創建: 同一個對象
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章