單例模式
單例模式(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
反序列化創建: 同一個對象