設計模式中的單例模式可以有7種寫法,這7種寫法有各自的優點和缺點: 代碼示例(java)及其分析如下: 一、懶漢式 優點: 不是馬上就初始化的,當需要使用的時候才進行初始化(即是lazy loading) 缺點: 在併發情況下是線程不安全的 二、懶漢式 ...
設計模式中的單例模式可以有7種寫法,這7種寫法有各自的優點和缺點:
代碼示例(java)及其分析如下:
一、懶漢式
public class Singleton
{
private static Singleton singleton;
private Singleton()
{
}
public static Singleton getInstance()
{
if (singleton == null)
singleton = new Singleton();
return singleton;
}
}
優點:
不是馬上就初始化的,當需要使用的時候才進行初始化(即是lazy loading)
缺點:
在併發情況下是線程不安全的
二、懶漢式線程安全版
public class Singleton
{
private static Singleton singleton;
private Singleton()
{
}
public synchronized static Singleton getInstance()
{
if (singleton == null)
singleton = new Singleton();
return singleton;
}
}
優點:
不是類載入之後就進行初始化的,當需要使用的時候才進行初始化(即是lazy loading),且為線程安全的
缺點:
效率低,加了synchronized進行同步之後,效率上有所降低
三、餓漢式
public class Singleton
{
private static Singleton singleton = new Singleton();
private Singleton()
{
}
public static Singleton getInstance()
{
return singleton;
}
}
這種方式基於classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用getInstance方法,但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到lazy loading的效果。其一個明顯的好處就是是線程安全的
四、餓漢式的變種寫法
public class Singleton
{
private static Singleton singleton;
static
{
singleton = new Singleton();
}
private Singleton()
{
}
public static Singleton getInstance()
{
return singleton;
}
}
其會在類載入的時候就進行載入。和上面的餓漢式的寫法優缺點相同
五、靜態內部類方式
public class Singleton
{
private Singleton()
{
}
private static class SingletonHolder
{
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance()
{
return SingletonHolder.singleton;
}
}
這種方式同樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟第三種和第四種方式不同的是(很細微的差別): 第三種和第四種方式是只要Singleton類被裝載了,那麼instance就會被實例化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過調用getInstance方法時,才會顯示裝載SingletonHolder類,從而實例化instance。想象一下,如果實例化instance很消耗資源,我想讓他延遲載入,另外一方面,我不希望在Singleton類載入時就實例化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被載入,那麼這個時候實例化instance顯然是不合適的。這個時候,這種方式相比第三和第四種方式就顯得很合理。
六、採用枚舉方式
public enum Singletons
{
INSTANCE;
// 此處表示單例對象裡面的各種方法
public void Method()
{
}
}
Effective Java作者Josh Bloch提倡使用枚舉的方式去實現單例模式。因為它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,同時寫法簡單。對於枚舉方式創建單例,為何可以避免多線程的同步以及防止反序列化重新創建新的對象這個原因,詳見相關博文:K:枚舉的線程安全性及其序列化問題
七、雙重校驗鎖
public class Singleton
{
private volatile static Singleton singleton;
private Singleton()
{
}
public static Singleton getInstance()
{
if (singleton == null)
{
synchronized (Singleton.class)
{
if (singleton == null)
{
singleton = new Singleton();
}
}
}
return singleton;
}
}
對於雙重校驗鎖,其是對懶漢式線程安全版的改進,其目的在於減少同步所用的開銷。對singleton進行兩次判null檢查,一次是在同步塊外,一次是在同步塊內。為什麼在同步塊內還要再檢驗一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個實例。
對singleton變數使用volatile關鍵字的原因是,instance = new Singleton()這句,並非是一個原子操作,事實上在 JVM中這句話大概做了下麵3件事情:
- 給 instance 分配記憶體
- 調用 Singleton的構造函數來初始化成員變數
- 將instance對象指向分配的記憶體空間(執行完這步instance就為非null了)
但是在 JVM的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是1-2-3也可能是1-3-2。如果是後者,則在3執行完畢、2未執行之前,被線程二搶占了,這時instance已經是非null了(但卻沒有初始化),所以線程二會直接返回instance,然後使用,然後順理成章地jvm就會報錯。
有些人認為使用volatile的原因是可見性,也就是可以保證線程在本地不會存有instance的副本,每次都是去主記憶體中讀取。但其實是不對的。使用volatile的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在volatile變數的賦值操作後面會有一個記憶體屏障(生成的彙編代碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完1-2-3之後或者1-3-2之後,不存在執行到1-3然後取到值的情況。從「先行發生原則」(即happen-before)的角度理解的話,就是對於一個volatile變數的寫操作都先行發生於後面對這個變數的讀操作(這裡的“後面”是時間上的先後順序)。
對於第一種和第二種寫法,實際上其可以歸類為懶漢式這一種寫法,對於第三種和第四種,其也可以歸為餓漢式這一種寫法。為此,一般單例都是五種寫法。懶漢,餓漢,雙重校驗鎖,枚舉和靜態內部類
對於單例模式,其有兩個問題需要註意:
如果單例由不同的類裝載器裝入,那便有可能存在多個單例類的實例。假定不是遠端存取,例如一些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));
}
對第二個問題修複的辦法是:
class Singletones implements java.io.Serializable
{
private static Singletones INSTANCE = new Singletones();
private Singletones()
{
}
public static Singletones getInstance()
{
return INSTANCE;
}
/*
* 我們反序列化後獲得的並不是原來的對象,而是經過重構的新的對象實例。
* ObjectInputStream對象在反序列化的時候,會在從I/O流中讀取對象時
* ,調用readResolve()方法。實際上就是用readResolve()中返回的對象直接替換在反序列化過程中重構的對象。
*/
private Object readResolve()
{
return Singletones.getInstance();
}
}
原因詳見博文:K:java中序列化的兩種方式—Serializable或Externalizable