對於一個軟件系統的某些類而言,咱們無須建立多個實例。舉個你們都熟知的例子——Windows任務管理器, 一般狀況下,不管咱們啓動任務管理多少次,Windows系統始終只能彈出一個任務管理器窗口,也就是說在一個Windows系統中,任務管理器存在惟一性。 還有好比說窗口上的工具箱,若是每次點擊工具箱按鈕都會建立一個工具箱實例,那麼窗口中會出現不少工具箱,可是咱們想要的是點擊工具箱若是沒有就出現,有了就再也不出現了,這就須要用到單例模式。html
下面是餓漢式單例類: android
package cglib;編程
public class SingletonClass {安全
private static final SingletonClass instance=new SingletonClass();
//私有構造函數
private SingletonClass(){}多線程
//禁止類的外部直接使用new來建立對象,所以須要將SingletonClass的構造函數的可見性改成//private
public static SingletonClass getInstance(){
return instance;
}
public static void main(String[] args){
SingletonClass s1=new SingletonClass();
SingletonClass s2=new SingletonClass();
SingletonClass s3=SingletonClass.getInstance();
SingletonClass s4=SingletonClass.getInstance();
System.out.println("s1:"+s1);
System.out.println("s2:"+s2);
System.out.println("s3:"+s3);
System.out.println("s4:"+s4);
System.out.println("instance:"+instance);
if(s3==s4){
System.out.println("s3,s4兩個實例相同");
System.out.println("s3:"+s3);
System.out.println("s4:"+s4);
}
}
}併發
輸出:編程語言
s1:cglib.SingletonClass@139a55
s2:cglib.SingletonClass@1db9742
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c
instance:cglib.SingletonClass@106d69c
s3,s4兩個實例相同
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c函數
須要注意的是getInstance()方法的修飾符,首先它應該是一個public方法,以便供外界其餘對象使用,其次它使用了static關鍵字,即它是一個靜態方法,在類外能夠直接經過類名來訪問,而無須建立 SingletonClass 對象,事實上在類外也沒法建立 SingletonClass 對象,由於構造函數是私有的。高併發
在類外咱們沒法直接建立新的 SingletonClass 對象,但能夠經過代碼 SingletonClass .getInstance()來訪問實例對象,第一次調用getInstance()方法時將建立惟一實例,再次調用時將返回第一次建立的實例,從而確保實例對象的惟一性。工具
因此:單例模式定義以下:
單例模式(Singleton Pattern):確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。單例模式是一種對象建立型模式。 |
單例模式有三個要點:一是某個類只能有一個實例;二是它必須自行建立這個實例;三是它必須自行向整個系統提供這個實例。
Singleton(單例):在單例類的內部實現只生成一個實例,同時它提供一個靜態的getInstance()工廠方法,讓客戶能夠訪問它的惟一實例;爲了防止在外部對其實例化,將其構造函數設計爲私有;在單例類內部定義了一個Singleton類型的靜態對象,做爲外部共享的惟一實例。
餓漢式單例因爲在定義靜態變量的時候實例化單例類,所以在類加載的時候就已經建立了單例對象,當類被加載時,靜態變量instance會被初始化,此時類的私有構造函數會被調用,單例類的惟一實例將被建立,可確保單例對象的惟一性。
下面是懶漢式單例模式:
package cglib;
public class SingletonClass {
private static SingletonClass instance=null;
//私有構造函數
private SingletonClass(){}
public synchronized static SingletonClass getInstance(){
if(instance==null){
System.out.println("第一次調用爲空instance:"+instance);
instance=new SingletonClass();
System.out.println("建立完instance:"+instance);
}
return instance;
}
public static void main(String[] args){
SingletonClass s1=new SingletonClass();
SingletonClass s2=new SingletonClass();
SingletonClass s3=SingletonClass.getInstance();
SingletonClass s4=SingletonClass.getInstance();
System.out.println("s1:"+s1);
System.out.println("s2:"+s2);
System.out.println("s3:"+s3);
System.out.println("s4:"+s4);
System.out.println("instance:"+instance);
if(s3==s4){
System.out.println("s3,s4兩個實例相同");
System.out.println("s3:"+s3);
System.out.println("s4:"+s4);
}
}
}
輸出:
第一次調用爲空instance:null
建立完instance:cglib.SingletonClass@139a55
s1:cglib.SingletonClass@1db9742
s2:cglib.SingletonClass@106d69c
s3:cglib.SingletonClass@139a55
s4:cglib.SingletonClass@139a55
instance:cglib.SingletonClass@139a55
s3,s4兩個實例相同
s3:cglib.SingletonClass@139a55
s4:cglib.SingletonClass@139a55
懶漢式單例在第一次調用getInstance()方法時實例化,在類加載時並不自行實例化,這種技術又稱爲延遲加載(Lazy Load)技術,即須要的時候再加載實例,爲了不多個線程同時調用getInstance()方法,咱們可使用關鍵字synchronized
該懶漢式單例類在getInstance()方法前面增長了關鍵字synchronized進行線程鎖,以處理多個線程同時訪問的問題。可是,上述代碼雖然解決了線程安全問題,可是每次調用getInstance()時都須要進行線程鎖定判斷,在多線程高併發訪問環境中,將會致使系統性能大大下降。如何既解決線程安全問題又不影響系統性能呢?咱們繼續對懶漢式單例進行改進。事實上,咱們無須對整個getInstance()方法進行鎖定,只需對其中的代碼「instance = new SingletonClass ();」進行鎖定便可。所以getInstance()方法能夠進行以下改進:
問題貌似得以解決,事實並不是如此。若是使用以上代碼來實現單例,仍是會存在單例對象不惟一。緣由以下:
假如在某一瞬間線程A和線程B都在調用getInstance()方法,此時instance對象爲null值,均能經過instance == null的判斷。因爲實現了synchronized加鎖機制,線程A進入synchronized鎖定的代碼中執行實例建立代碼,線程B處於排隊等待狀態,必須等待線程A執行完畢後才能夠進入synchronized鎖定代碼。但當A執行完畢時,線程B並不知道實例已經建立,將繼續建立新的實例,致使產生多個單例對象,違背單例模式的設計思想,所以須要進行進一步改進,在synchronized中再進行一次(instance == null)判斷,這種方式稱爲雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實現的懶漢式單例類完整代碼以下所示:
須要注意的是,若是使用雙重檢查鎖定來實現懶漢式單例類,須要在靜態成員變量instance以前增長修飾符volatile,被volatile修飾的成員變量能夠確保多個線程都可以正確處理,且該代碼只能在JDK 1.5及以上版本中才能正確執行。因爲volatile關鍵字會屏蔽Java虛擬機所作的一些代碼優化,可能會致使系統運行效率下降,所以即便使用雙重檢查鎖定來實現單例模式也不是一種完美的實現方式。
餓漢式單例類與懶漢式單例類比較
餓漢式單例類在類被加載時就將本身實例化,它的優勢在 於無須考慮多線程訪問問題,能夠確保實例的惟一性;從調用速度和反應時間角度來說,因爲單例對象一開始就得以建立,所以要優於懶漢式單例。可是不管系統在 運行時是否須要使用該單例對象,因爲在類加載時該對象就須要建立,所以從資源利用效率角度來說,餓漢式單例不及懶漢式單例,並且在系統加載時因爲須要建立 餓漢式單例對象,加載時間可能會比較長。
懶漢式單例類在 第一次使用時建立,無須一直佔用系統資源,實現了延遲加載,可是必須處理好多個線程同時訪問的問題,特別是當單例類做爲資源控制器,在實例化時必然涉及資 源初始化,而資源初始化頗有可能耗費大量時間,這意味着出現多線程同時首次引用此類的機率變得較大,須要經過雙重檢查鎖定等機制進行控制,這將致使系統性 能受到必定影響。
一種更好的單例實現方法
在IoDH中,咱們在單例類中增長一個靜態(static)內部類,在該內部類中建立單例對象,再將該單例對象經過getInstance()方法返回給外部使用,實現代碼以下所示:
package cglib;
public class SingletonClass {
private SingletonClass () {
}
private static class SingletonClassHolder {
private final static SingletonClass instance = new SingletonClass ();
}
public static SingletonClass getInstance() {
return SingletonClassHolder.instance;
}
public static void main(String[] args){
SingletonClass s1=new SingletonClass();
SingletonClass s2=new SingletonClass();
SingletonClass s3=SingletonClass.getInstance();
SingletonClass s4=SingletonClass.getInstance();
System.out.println("s1:"+s1);
System.out.println("s2:"+s2);
System.out.println("s3:"+s3);
System.out.println("s4:"+s4);
System.out.println("SingletonClassHolder.instance:"+SingletonClassHolder.instance);
if(s3==s4){
System.out.println("s3,s4兩個實例相同");
System.out.println("s3:"+s3);
System.out.println("s4:"+s4);
}
輸出:
s1:cglib.SingletonClass@139a55
s2:cglib.SingletonClass@1db9742
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c
SingletonClassHolder.instance:cglib.SingletonClass@106d69c
s3,s4兩個實例相同
s3:cglib.SingletonClass@106d69c
s4:cglib.SingletonClass@106d69c
編譯並運行上述代碼,運行結果爲:true,即建立的單例對象s1和s2爲同一對象。因爲靜態單例對象沒有做爲 SingletonClass 的成員變量直接實例化,所以類加載時不會實例化Singleton,第一次調用getInstance()時將加載內部類 SingletonClassHolder ,在該內部類中定義了一個static類型的變量instance,此時會首先初始化這個成員變量,由Java虛擬機來保證其線程安全性,確保該成員變量只能初始化一次。因爲getInstance()方法沒有任何線程鎖定,所以其性能不會形成任何影響。
經過使用IoDH,咱們既能夠實現延遲加載,又能夠保證線程安全,不影響系統性能,不失爲一種最好的Java語言單例模式實現方式(其缺點是與編程語言自己的特性相關,不少面嚮對象語言不支持IoDH)。
可是,上面提到的全部實現方式都有兩個共同的缺點:
固然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法:
public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
使用枚舉除了線程安全和防止反射強行調用構造器以外,還提供了自動序列化機制,防止反序列化的時候建立新的對象。所以,Effective Java推薦儘量地使用枚舉來實現單例。
可是在Android平臺上倒是不被推薦的。在這篇Android Training中明確指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再好比雙重檢查鎖法,不能在jdk1.5以前使用,而在Android平臺上使用就比較放心了(通常Android都是jdk1.6以上了,不只修正了volatile的語義問題,還加入了很多鎖優化,使得多線程同步的開銷下降很多)。
因此,無論採起何種方案,請時刻牢記單例的三大要點:
1.主要優勢
單例模式的主要優勢以下:
(1) 單例模式提供了對惟一實例的受控訪問。由於單例類封裝了它的惟一實例,因此它能夠嚴格控制客戶怎樣以及什麼時候訪問它。
(2) 因爲在系統內存中只存在一個對象,所以能夠節約系統資源,對於一些須要頻繁建立和銷燬的對象單例模式無疑能夠提升系統的性能。
(3) 容許可變數目的實例。基於單例模式咱們能夠進行擴展,使用與單例控制類似的方法來得到指定個數的對象實例,既節省系統資源,又解決了單例單例對象共享過多有損性能的問題。
2.主要缺點
單例模式的主要缺點以下:
(1) 因爲單例模式中沒有抽象層,所以單例類的擴展有很大的困難。
(2) 單例類的職責太重,在必定程度上違背了「單一職責原則」。由於單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的建立和產品的自己的功能融合到一塊兒。
(3) 如今不少面嚮對象語言(如Java、C#)的運行環境都提供了自動垃圾回收的技術,所以,若是實例化的共享對象長時間不被利用,系統會認爲它是垃圾,會自動銷燬並回收資源,下次利用時又將從新實例化,這將致使共享的單例對象狀態的丟失。
3.適用場景
在如下狀況下能夠考慮使用單例模式:
(1) 系統只須要一個實例對象,如系統要求提供一個惟一的序列號生成器或資源管理器,或者須要考慮資源消耗太大而只容許建立一個對象。
(2) 客戶調用類的單個實例只容許使用一個公共訪問點,除了該公共訪問點,不能經過其餘途徑訪問該實例。