深入解析單例設計模式
文章目錄
1.單例設計模式
-
1.1 單例設計模式的定義:
保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點
-
1.2單例設計模式的要點有三個:
- 是某個類只能有一個例項;
- 是它必須自行建立這個例項;
- 是它必須自行向整個系統提供這個例項。
-
1.3從具體實現角度來說,就是以下三點:
- 是單例模式的類只提供私有的建構函式
- 是類定義中含有一個該類的靜態私有物件
- 是該類提供了一個靜態的公有的函式用於建立或獲取它本身的靜態私有物件。
上述是一個廣義的概念,那麼在具體開發中單例帶來了什麼呢?
2.在java語言中,單例帶來了兩大好處:
-
對於頻繁使用的物件,可以省略建立物件所花費的時間,這對於那些重量級的物件而言,是非常可觀的一筆系統開銷。
-
由於new操作的次數減少,因而對系統記憶體的使用頻率也會降低,這將減輕GC壓力,縮短GC停頓時間。
所以對於系統的關鍵元件和被頻繁操作的物件,使用單例模式便可以有效地改善系統性能。
單例的參與者非常簡單,只有單例類和使用者兩個;
3.下面介紹一下幾種實現單例設計模式的寫法
3.1 懶漢,執行緒不安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
這種寫法能夠在多執行緒中很好的工作,而且看起來它也具備很好的lazy loading,但是,遺憾的是,效率很低,99%情況下不需要同步。
3.2 餓漢
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
這種方式基於classloder機制避免了多執行緒的同步問題,instance在類裝載時就例項化。目前java單例是指一個虛擬機器的範圍,因為裝載類的功能是虛擬機器的,所以一個虛擬機器在通過自己的ClassLoader裝載餓漢式實現單例類的時候就會建立一個類的例項。這就意味著一個虛擬機器裡面有很多ClassLoader,而這些classloader都能裝載某個類的話,就算這個類是單例,也能產生很多例項。當然如果一臺機器上有很多虛擬機器,那麼每個虛擬機器中都有至少一個這個類的例項的話,那這樣 就更不會是單例了。(這裡討論的單例不適合叢集!)
3.3 靜態內部類
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這種方式同樣利用了classloder的機制來保證初始化instance時只有一個執行緒,這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過呼叫getInstance方法時,才會顯示裝載SingletonHolder類,從而例項化instance。想象一下,如果例項化instance很消耗資源,我想讓他延遲載入!這個時候,這種方式相比第2種方式就顯得很合理。
3.4 列舉
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件,可謂是很堅強的壁壘啊,不過,個人認為由於1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這麼寫過。
3.5 雙重校驗鎖(jdk1.5)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
這樣方式實現執行緒安全地建立例項,而又不會對效能造成太大影響。它只是第一次建立例項的時候同步,以後就不需要同步了。
由於volatile關鍵字遮蔽了虛擬機器中一些必要的程式碼優化,所以執行效率並不是很高,因此建議沒有特別的需要不要使用。雙重檢驗鎖方式的單例不建議大量使用,根據情況決定。
4.總結
有兩個問題需要注意:
-
如果單例由不同的類裝載器裝入,那便有可能存在多個單例類的例項。假定不是遠端存取,例如一些servlet容器對每個servlet使用完全不同的類裝載器,這樣的話如果有兩個servlet訪問一個單例類,它們就都會有各自的例項。
-
如果Singleton實現了java.io.Serializable介面,那麼這個類的例項就可能被序列化和復原。不管怎樣,如果你序列化一個單例類的物件,接下來複原多個那個物件,那你就會有多個單例類的例項。
對第一個問題修復的辦法是:
private static Class getClass(String classname)
throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}
對第二個問題修復的辦法是:
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
private Object readResolve() {
return INSTANCE;
}
}