模式-單例模式-java
目錄
前言
最近看Effective Java看到了一點關于單例模式的內容,結合自己所知,在此做個總結歸納。
單例模式
單例模式(Singleton Pattern)是一種常用的軟件設計模式,用于限制一個類的實例化次數,確保在整個程序運行期間,該類只有一個實例存在,并提供一個全局訪問點來訪問這個實例。
單例模式的幾種實現方式
普遍來說,單例模式的實現主要有兩種方式:
- ? 餓漢式:類加載時該單實例對象被創建。
- 懶漢式:首次使用該對象時,該單實例對象才會被創建。
補充:
- 枚舉
餓漢式
- 思路:在類加載時就創建好一個靜態實例,因此類加載器保證了單例的唯一性。
餓漢式-靜態變量寫法
- 優點:簡單易懂,不需要加鎖。
- 缺點:無論是否需要,都會在類加載時創建實例,可能造成資源浪費。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
餓漢式-靜態代碼塊寫法
- 實現邏輯與靜態變量寫法寫法基本一致,優缺點同上。
public class Singleton {
private static final Singleton INSTANCE ;
static{
INSTANCE = new Singleton()
}
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
懶漢式
- 總體思想:在第一次使用時才創建實例。
懶漢式-經典寫法
- 思想:第一次使用時才創建實例。
- 優點:可以延遲加載。
- 缺點:在多線程環境下可能會產生多個實例,需要加鎖來保證線程安全。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {//在多線程情況下,理論上有可能出現多個線程同時進入了該判斷體。
instance = new Singleton();
}
return instance;
}
}
懶漢式-同步方法(不推薦)
- 思想:在每次調用getInstance方法時都進行同步,從而確保即使在多線程環境下,也不會創建出多個實例。
- 優點:延遲加載、線程安全。
- 缺點:我們知道,絕大大部分情況下資源沖突并不會頻繁發生,而每次調用getInstance方法時都需要進行同步操作,這會導致性能下降。尤其是在高并發情況下,同步操作可能會成為瓶頸。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {//在多線程情況下,理論上有可能出現多個線程同時進入了該判斷體。
instance = new Singleton();
}
return instance;
}
}
懶漢式-雙重檢查鎖(推薦)
- 思想:使用雙重檢查鎖定(Double-Checked Locking),只在必要時進行同步。
- 優點:延遲加載,并且線程安全。
- 缺點:實現稍微復雜一些。
public class Singleton {
private volatile static Singleton instance;//注意volatile
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {//檢查是否未初始化
synchronized (Singleton.class) {//注意,在一個線程拿到鎖后,可能有多個線程阻塞在該部分,在當前線程完成初始化操作后,他們也是有機會拿到鎖的,因而需要在鎖內部再加一個判斷
if (instance == null) {//為了保證其他線程對instance的可見性,instance 應該聲明為volatile
instance = new Singleton();
}
}
}
return instance;
}
}
注意:
- 在雙重檢查鎖定中,instance變量需要被聲明為volatile,以確保多線程環境下對instance的可見性和有序性
懶漢式-靜態內部類(推薦)
- 思想:控制類加載的時機,利用類加載機制保證初始化實例時只有一個線程。
- 優點:既實現了懶加載,又能保證線程安全,而且代碼簡潔。
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
存在的問題
簡單來說,私有構造方法并不是絕對安全的,仍可通過一定方式拿到它,請看如下代碼:
public static void main(String[] args) throws Exception {
Singleton s = Singleton.getInstance();//這個Singleton可以是以上某一個
Constructor<Singleton > constructor = Singleton .class.getDeclaredConstructor();
constructor.setAccessible(true);//打開可訪問性
Singleton sf = constructor.newInstance();//獲取其無參構造方法
System.out.println(s == sf);//比較引用
}
在此情況下,我們通過反射拿到了該類的一個實例,與原實例比較引用,會發現,其指向并不相同。
當然,反射問題屬于比較極端的問題。但是,其在序列化和反序列化下也并不安全,假設我們的實例類實現了 Serializable接口(以上代碼未實現)。
Singleton s = Singleton.getInstance();//這個Singleton可以是以上某一個
byte[] serialize = SerializationUtils.serialize(s);
Object deserialize = SerializationUtils.deserialize(serialize);
System.out.println(s == deserialize); //true or false --> false
與原實例比較引用,會發現,其指向并不相同。關于這一部分,實際上有個機制:
當一個實現了Serializable接口的單例對象被序列化后,再通過反序列化操作恢復時,默認情況下會生成一個新的對象實例。這意味著即使原始對象是單例的,反序列化后的對象也將是一個新的實例。
為了保證序列化安全,需要在單例類中定義readResolve方法。這個方法將在反序列化過程中被調用,用來返回一個替代對象。通過readResolve方法返回單例的現有實例,可以確保序列化和反序列化過程中始終只有一個實例。如:
public class Singletonimplements Serializable {
private static class LazyHolder {
private static final SingletonINSTANCE = new MyTest();
}
private Singleton() {
}
public static final SingletongetInstance() {
return LazyHolder.INSTANCE;
}
private Object readResolve() {//see
return LazyHolder.INSTANCE;
}
}
枚舉(天然適合)
事實上,枚舉實現單例模式的方式是基于語言級別的支持,它不僅簡潔,而且天然具備線程安全性和序列化安全性。
比如:
- 反射安全方面:在Constructor源碼中,當調用newInstance創建對象時,會檢查該類是否為ENUM,如果是則拋出異常,也就是說即使拿到了該枚舉類的構造方法,也無法通過反射來建立它的實例。
- 序列化安全方面:當枚舉對象被序列化時,只會將枚舉常量的名字(name)輸出到結果中,而不是整個對象的狀態。在反序列化時,Java 會通過調用 java.lang.Enum.valueOf(Class, String) 方法來根據名字查找枚舉常量,而不是創建一個新的枚舉對象。。
- 線程安全:由反編譯可知,枚舉常量的初始化是在靜態代碼塊中完成的,在類加載時完成初始化,而類加載是由JVM保證線程安全的。
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("Elvis has left the building.");
}
}
總結
- 餓漢式:在類加載時創建實例,簡單易懂,無需加鎖。
- 懶漢式:延遲創建實例,需考慮線程安全問題。
- 經典寫法:非線程安全。
- 同步方法:線程安全但性能較差。
- 雙重檢查鎖(DCL):線程安全且性能較好。
- 靜態內部類:線程安全且簡潔。
- 枚舉:簡潔且天然具備線程安全性和序列化安全性,防止反射破壞。

浙公網安備 33010602011771號