單例模式(Singleton Pattern
)
意圖
確保一個類只有一個實例,並提供一個全局訪問點。
單例模式的三個要點:1.單例類只有一個實例對象;2.該單例對象必須由單例類自行創建;3.單例類對外提供一個訪問該單例的全局訪問點。
動機
對於系統中的某些類來說,只能有一個對象,例如:線程池、緩存、註冊表的對象等。若製造出多個實例,就會導致許多問題的產生,如:程序行爲異常、資源使用過度、結果不一致等。
如何保證一個對象只能被實例化一次?
利用靜態變量、靜態方法和適當的訪問修飾符,可以實現。而更好的方法是讓單例類自身負責管理它的唯一實例。
適用性
單例常常用來管理共享的資源,例如:Windows 的回收站、操作系統中的文件系統、顯卡的驅動程序對象、打印機的後臺處理服務、應用程序的日誌對象、數據庫的連接池、網站的計數器、Web 應用的配置對象、應用程序中的對話框等常常被設計成單例。
結構
實現
- 餓漢式單例
public class Singleton {
// 私有的靜態屬性
private static Singleton instance = new Singleton();
// 私有的構造方法
private Singleton() {
}
// 公共的靜態方法,實例的全局訪問點
public static Singleton getInstance() {
return instance;
}
}
類一旦加載就創建一個實例,保證在調用 getInstance 方法之前實例已經存在了。線程不安全問題主要是由於 instance 被實例化多次,採取直接實例化 instance 的方式就不會產生線程不安全問題。但是直接實例化的方式也丟失了延遲實例化帶來的節約資源的好處。
- 懶漢式單例(非線程安全)
public class Singleton {
//私有靜態變量 instance 被延遲實例化
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
// 如果多個線程能夠同時進入,將導致實例化多次 instance
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
私有靜態變量 instance 被延遲實例化,他好處是,如果沒有用到該類,那麼就不會實例化,從而節約資源。但是這個實現在多線程環境下是不安全的,如果多個線程能夠同時進入 if (instance == null) ,並且此時 instance 爲 null,那麼會有多個線程執行 instance = new Singleton();
- 雙重校驗鎖機制(線程安全)
public class Singleton {
// volatile修飾共享變量,保證了不同線程對變量進行操作時的可見性,可以禁止 JVM 的指令重排序
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getUniqueInstance() {
// 第一次檢查,用來避免 instance 已經被實例化也去做同步操作
if (instance != null) {
// 不創建
} else {
synchronized (Singleton.class) {
// 第二次檢查,加鎖,確保只有一個線程進行實例化操作
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
雙重校驗鎖(Double Check Locking
,簡稱DCL
),資源利用率高,第一次執行getInstance時單例對象才被實例化;但是第一次加載時反應稍慢一些,在高併發環境下也有一定的缺陷,雖然發生的概率很小。DCL雖然在一定程度解決了資源的消耗、多餘的同步和線程安全等問題,但是他還是在某些情況會出現失效的問題,也就是DCL失效。
- 靜態內部類單例模式(線程安全)
public class Singleton {
// 靜態內部類,只在第一次使用時進行類加載
private static class MySingletonFactory {
// 使用final修飾,可以避免變量被重新賦值,JVM也不用去跟蹤該引用是否被更改
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return MySingletonFactory.INSTANCE;
}
}
靜態內部類,只在第一次使用時進行類加載,也就是說當調用 getInstance() 從而觸發 MySingletonFactory.INSTANCE 從而觸發時 MySingletonFactory 纔會被加載,此時初始化 INSTANCE 實例,且 JVM 能確保 INSTANCE 只被實例化一次。(不僅可以延遲實例化,而且JVM 提供了對線程安全的支持)。
- 序列化與反序列化實現單例模式(線程安全)
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static class MySingletonFactory {
private static final SerializeSingleton instance = new Singleton();
}
private Singleton() {}
public static SerializeSingleton getInstance() {
return MySingletonFactory.instance;
}
// 不加readResolve(),默認的方式運行得到的結果就是多例的
protected Object readResolve() throws ObjectStreamException {
System.out.println("調用了readResolve方法!");
return MySingletonFactory.instance;
}
}
// 測試僞代碼
public class Test{
public static void main(String[] args) throws {
Singleton singleton = Singleton.getInstance();
File file = new File("MySingleton.txt");
// 序列化
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton);
System.out.println("序列化 hashCode: "+singleton.hashCode());
// 反序列化
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton rSingleton = (Singleton) ois.readObject();
System.out.println("反序列化 hashCode: "+rSingleton.hashCode());
// 資源釋放
}
}
在反序列化時,ObjectInputStream
的 readObject()
的內部代碼執行順序:readObject() --> readObject0() --> readOrdinaryObject() --> invokeReadResolve() --> readResolveMethod.invoke()
invokeReadResolve()
中 使用 readResolveMethod.invoke()
克隆對象。換句話說,invokeReadResolve()
使用反射機制創建新的對象,從而破壞了單例唯一性。
readResolve()
會在 ObjectInputStream
會檢查對象的 class
是否定義了 readResolve()
。如果定義了,將由 readResolve()
指定返回的對象。返回對象的類型一定要是兼容的,否則會拋出 ClassCastException
。
- 枚舉實現單例模式(線程安全)
public enum EnumSingleton implements SingletonInterface {
INSTANCE {
@Override
public void doSomething() {
System.out.println("EnumSingleton singleton");
}
};
public static EnumSingleton getInstance() {
return EnumSingleton.INSTANCE;
}
}
public interface SingletonInterface {
void doSomething();
}
已知應用
java.lang.Runtime
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
// ......
}
@Test
public void testRuntime() {
Runtime r1 = Runtime.getRuntime();
Runtime r2 = Runtime.getRuntime();
//“==”爲地址判斷,爲true表示:r1與r2指向同一對象。
System.out.println(r1 == r2); // 輸出 true
}
以上爲 java.lang.Runtime
類的部分代碼,是餓漢式單例模式,在該類第一次被 classloader
的時候創建唯一實例。
Runtime
類封裝了 Java
運行時的環境。每一個 Java
程序實際上都是啓動了一個 JVM
進程,每個 Java
程序都有一個 Runtime
類實例,使應用程序能夠與其運行的環境相連接。由於Java
是單進程的,所以,在一個JVM
中,Runtime
的實例應該只有一個。
Spring
依賴注入Bean
實例(默認單例)