場景描述:假設我們需要一個全局唯一的實例,比如線程池,緩存等。
初步解決:比如所有開發人員通過約定,或者全局變量來實現。
問題:將對象賦值給一個全局變量,但並不一定會立即使用它,從而佔用內存資源,造成資源的浪費。
更好的方案:使用單例模式。
經典的單例模式代碼如下:
public class Singleton {
private static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance(){
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
單件模式確保一個類只有一個實例,並提供一個全局訪問點。
這種做法對資源敏感的對象特別重要。
相應的類圖:
新的問題:假設我們的程序是一個多線程的程序,如果使用上述經典代碼就會出現多個單例對象的奇特情況。因爲我們缺少了同步。
第一種解決方案:將方法變爲同步方法。
public class Singleton_One {
private static Singleton_One uniqueInstance;
private Singleton_One(){}
private static synchronized Singleton_One getInstance(){
if (uniqueInstance == null){
uniqueInstance = new Singleton_One();
}
return uniqueInstance;
}
}
這種方法帶來的缺陷是每次訪問方法都會進行同步,造成性能的下降。而我們實際上只需要在第一次進行同步即可。
第二種方法,不使用延遲實例化,而是急切的創建實例。
public class Singleton_Two {
private static Singleton_Two uniqueInstance = new Singleton_Two();
private Singleton_Two(){}
public static Singleton_Two getInstance(){
return uniqueInstance;
}
}
這種方法的缺陷類似於使用全局變量的問題,雖然提前創建了實例,但我們並不能保證創建了後就立即使用,也會造成一定程度的浪費。
第三種方法,雙重檢查加鎖,曾經的一種經典做法。
public class Singleton_Three {
private volatile static Singleton_Three uniqueInstance;
private Singleton_Three(){}
public static Singleton_Three getInstance(){
if (uniqueInstance == null){
synchronized (Singleton_Three.class){
if (uniqueInstance == null){
uniqueInstance = new Singleton_Three();
}
}
}
return uniqueInstance;
}
}
只有在第一次進行同步,並且使用了volatile變量避免了重排序造成的問題,但一方面由於其自身的缺陷,另一方面同步性能問題的提升,導致這種解決方案已經變爲歷史,不推薦使用,複雜且難以理解。
第四種方法,延遲初始化佔位類。
public class Singleton_Four {
private static class ResourceHolder{
public static Resource resource = new Resource();
}
public static Resource getResource(){
return ResourceHolder.resource;
}
}
public class Resource {
// 一個資源類,需要被單例加載
}
當任何線程第一次調用getResource時,都會使ResourceHolder被加載和被初始化,此時靜態初始化器將執行Resource的初始化操作。這種方法在第二種提前實例化的方法上改進而來,並且不需要同步。因此最推薦在多線程情況下使用該方法。
注意:每個類加載器都定義了一個命名空間,如果有兩個以上的類加載器,不同的類加載器可能會加載同一個類,從整個程序來看,同一個類會被加載多次。如果這樣的事情發生在單例上,就會產生多個單例並存的怪異現象。所以如果你的程序有多個類加載器又同時使用了單例模式,請小心。
總結:當你的程序中迫切需要在全局只需一個實例對象的情況時,考慮使用單例模式,並推薦使用延遲初始化佔位類方式避免多線程問題。但過度使用單例模式有可能適得其反,所以在設計時需要慎重考慮。