本文介紹了單例模式及其4種推薦寫法(餓漢模式,雙重校驗鎖(DCL),Holder模式(靜態內部類)和枚舉模式)和3類保護手段(反序列化,反射,自定義類加載器)
單例模式(Singleton Pattern)的定義:
Ensure a class has only one instance,and provide a global point of access to it.
確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例.
使用單例模式的優點
- 節省內存
- 減少性能開銷
- 避免對資源的多重佔用
單例模式的使用場景
- 無狀態的工具類:比如日誌工具類,不管是在哪裏使用,我們需要的只是它幫我們記錄日誌信息,除此之外,並不需要在它的實例對象上存儲任何狀態,這時候我們就只需要一個實例對象即可。
- 全局信息類:比如我們在一個類上記錄網站的訪問次數,我們不希望有的訪問被記錄在對象A上,有的卻記錄在對象B上,這時候我們就讓這個類成爲單例。
- 實例化需要消耗過多資源的類
- 要求生成唯一序列化的環境
值得注意的是,單例往往都可以通過直接聲明爲static來實現,把一個實例方法變成靜態方法,或者把一個實例變量變成靜態變量,都可以起到單例的效果。這只是面向對象和麪向過程的區別。
單例模式的4種推薦寫法
這裏列舉了單例模式的4種安全的推薦寫法,另外也可以直接在獲取單例的方法上加synchronized,這種方法雖然安全但效率極低,不值得推薦.這裏4種推薦寫法分別爲餓漢模式,雙重校驗鎖(DCL),Holder模式(靜態內部類)和枚舉模式.
1. 餓漢模式
public class Singleton {
private final static Singleton INSTANCE= new Singleton ();
private Singleton () {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
- 原理:通過類加載機制來保證單例,在類被加載的時候創建INSTANCE對象
- 優點:較簡單
- 缺點:導致類裝載的原因有很多種,在單例模式中大多數都是調用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到lazy loading的效果。
2.雙重校驗鎖(DCL)
public class Singleton {
private volatile static Singleton instance = null;
private Singleton () {
}
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 原理:通過雙重鎖機制和volatile關鍵字保證了單例
- 優點:只有需要單例對象又沒有單例對象的時候纔會創建,最大化地實現了lazy loading
- 缺點:複雜,效果和Holder模式相同,且更不易理解(雖然有些書可能會說加了volatile會影響性能,但其實影響微乎其微)
3.Holder模式(靜態內部類)
public class Singleton {
private static class SingletonHolder {
public static final Singleton INSTANCE = new Singleton();
}
private Singleton (){
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 原理:又稱爲”延遲初始化佔位類模式”,顧名思義,就是通過一個內部類佔位來實現延遲初始化,原理也是通過類加載機制來保證單例,和餓漢模式的區別在於用一個內部類來減小由於類的其他靜態方法被調用而導致單例被提前創建的可能性.
- 優點:最大化地實現了lazy loading,且更簡單
5.枚舉模式
public enum Singleton {
INSTANCE;
}
- 原理:通過枚舉的語言特性保證單例
- 優點:簡潔,從JVM層面保證單例,更安全(不存在序列化,反射破壞單例的情況)
4種單例模式實現的比較
單例寫法 | 單例保障機制 | 單例對象初始化時機 | 優點 | 缺點 |
---|---|---|---|---|
餓漢模式 | 類加載機制 | 類加載 | 簡單,易理解 | 難以保證懶加載,無法應對反射和反序列化 |
雙重校驗鎖(DCL) | 鎖機制(需volatile防止重排序) | 第一次調用getInstance() | 實現懶加載 | 複雜,無法應對反射和反序列化 |
Holder模式(靜態內部類) | 類加載機制 | 第一次調用getInstance() | 實現懶加載 | 無法應對反射和反序列化 |
枚舉 | 枚舉語言特性 | 第一次引用枚舉對象 | 簡潔,安全(語言級別防止通過反射和反序列化破壞單例) |
防止非法創建單例對象的手段:
1.反序列化
前三種寫法在實現序列化(implements java.io.Serializable)時爲保證單例都需要重寫readResolve()方法,如
public class Singleton implements java.io.Serializable {
public final static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//反序列化時直接返回當前INSTANCE而不是反序列化出來的對象
private Object readResolve() {
return INSTANCE;
}
}
然後再通過例子看看枚舉爲什麼不會受到反序列化的破解
public enum SingletonEnum {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
在枚舉類型的序列化和反序列化上,Java做了特殊的規定:
在序列化時Java僅僅是將枚舉對象的name屬性輸出到結果中,
反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。
同時,編譯器是不允許任何對這種序列化機制的定製的並禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,從而保證了枚舉實例的唯一性
2.反射
前三種寫法默認無法應對反射(先把私有的構造器設爲accessible,再用newInstance()方法),但newInstance()也是通過new來獲得實例的,所以可以從構造方法入手,進行保護,如
public class Singleton implements java.io.Serializable {
public final static Singleton INSTANCE = new Singleton();
//初始化標識
private static volatile boolean flag = true;
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//在構建實例之前判斷是否已經初始化過了,如果初始化過一次構造方法仍被調用則拋出異常
private Singleton(){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists !");
}
}
}
再解釋一下枚舉單例不受反射破壞的原因,直接看源碼:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
//這裏判斷Modifier.ENUM是不是枚舉修飾符,如果是就拋異常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
說明確實無法使用反射創建枚舉實例
3.使用不同的類加載器
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性
—— 《深入理解Java虛擬機》 第7章- 虛擬機類加載機制
如果是使用自定義的類加載器進行加載,就可以產生新的實例.(枚舉也無能爲力)
假定不是遠端存取,例如一些servlet容器對每個servlet使用完全不同的類裝載器,這樣的話如果有兩個servlet訪問一個單例類,它們就都會有各自的實例。
這裏有個獲得單例類對象的方法
private static Class getClass(String classname)
throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}
這段代碼目的是每次獲取的時候能獲得同一個類加載器,然後再通過這個加載器去加載單例類來保證唯一性,所以這只是提供了獲取同一個類加載器加載的單例類的途徑,而非提供保護.
但是,從另一個方面來理解,使用不同的類加載器獲得的本身就不是同一個類了,不同的類的實例仍然各只有一個,從這個角度看單例模式也不算被破壞,總不能要求兩個不同的類的實例之和只能爲1吧.
參考:
http://cantellow.iteye.com/blog/838473
https://zhuanlan.zhihu.com/p/32310340
https://blog.csdn.net/javazejian/article/details/71333103
《深入理解Java虛擬機》–周志明