一. 什么是單例模式
只需要某個類同時保留一個對象,不希望有更多對象,此時,我們則應考慮單例模式的設計。
單例模式的主要作用是保證在Java程序中,某個類只有一個實例存在。
單例模式有很多好處,它能夠避免實例對象的重復創建,不僅可以減少每次創建對象的時間開銷,還可以節約內存空間;
能夠避免由於操作多個實例導致的邏輯錯誤。如果一個對象有可能貫穿整個應用程序,而且起到了全局統一管理控制的作用,那么單例模式也許是一個值得考慮的選擇。
二. 單例模式的特點
1. 單例模式只能有一個實例。
2. 單例類必須創建自己的唯一實例。
3. 單例類必須向其他對象提供這一實例。
三. 單例模式VS靜態類
在知道了什么是單例模式后,我想你一定會想到靜態類,“既然只使用一個對象,為何不干脆使用靜態類?”,這里我會將單例模式和靜態類進行一個比較。
1. 單例可以繼承和被繼承,方法可以被override,而靜態方法不可以。
2. 靜態方法中產生的對象會在執行后被釋放,進而被GC清理,不會一直存在於內存中。
3. 靜態類會在第一次運行時初始化,單例模式可以有其他的選擇,即可以延遲加載。
4. 基於2, 3條,由於單例對象往往存在於DAO層(例如sessionFactory),如果反復的初始化和釋放,則會占用很多資源,而使用單例模式將其常駐於內存可以更加節約資源。
5. 靜態方法有更高的訪問效率。
6. 單例模式很容易被測試。
幾個關於靜態類的誤解:
誤解一:靜態方法常駐內存而實例方法不是。
實際上,特殊編寫的實例方法可以常駐內存,而靜態方法需要不斷初始化和釋放。
誤解二:靜態方法在堆(heap)上,實例方法在棧(stack)上。
實際上,都是加載到特殊的不可寫的代碼內存區域中。
靜態類和單例模式情景的選擇:
情景一:不需要維持任何狀態,僅僅用於全局訪問,此時更適合使用靜態類。
情景二:需要維持一些特定的狀態,此時更適合使用單例模式。
單例模型的寫法
1、餓漢模式
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton newInstance(){
return instance;
}
}
注解:初試化靜態的instance創建一次。如果我們在Singleton類里面寫一個靜態的方法不需要創建實例,它仍然會早早的創建一次實例。而降低內存的使用率。
缺點:沒有lazy loading的效果,從而降低內存的使用率。
2、懶漢模式
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton newInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
注解:Singleton的靜態屬性instance中,只有instance為null的時候才創建一個實例,構造函數私有,確保每次都只創建一個,避免重復創建。
缺點:只在單線程的情況下正常運行,在多線程的情況下,就會出問題。例如:當兩個線程同時運行到判斷instance是否為空的if語句,並且instance確實沒有創建好時,那么兩個線程都會創建一個實例,因此需要加鎖解決線程同步問題,實現如下:
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton newInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
3、雙重校驗鎖
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {//2
instance = new Singleton();
}
}
}
return instance;
}
}
注解:只有當instance為null時,需要獲取同步鎖,創建一次實例。當實例被創建,則無需試圖加鎖。
缺點:用雙重if判斷,復雜,容易出錯。
4、靜態內部類
public class Singleton{
private static class SingletonHolder{
public static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton newInstance(){
return SingletonHolder.instance;
}
}
這種方式同樣利用了類加載機制來保證只創建一個instance實例。它與餓漢模式一樣,也是利用了類加載機制,因此不存在多線程並發的問題。不一樣的是,它是在內部類里面去創建對象實例。這樣的話,只要應用中不使用內部類,JVM就不會去加載這個單例類,也就不會創建單例對象,從而實現懶漢式的延遲加載。也就是說這種方式可以同時保證延遲加載和線程安全。
5、枚舉
public enum Singleton{
instance;
public void whateverMethod(){}
}
上面提到的四種實現單例的方式都有共同的缺點:
1)需要額外的工作來實現序列化,否則每次反序列化一個序列化的對象時都會創建一個新的實例。
2)可以使用反射強行調用私有構造器(如果要避免這種情況,可以修改構造器,讓它在創建第二個實例的時候拋異常)。
而枚舉類很好的解決了這兩個問題,使用枚舉除了線程安全和防止反射調用構造器之外,還提供了自動序列化機制,防止反序列化的時候創建新的對象。因此,《Effective Java》作者推薦使用的方法。不過,在實際工作中,很少看見有人這么寫。
總結
本文總結了五種Java中實現單例的方法,其中前兩種都不夠完美,雙重校驗鎖和靜態內部類的方式可以解決大部分問題,平時工作中使用的最多的也是這兩種方式。枚舉方式雖然很完美的解決了各種問題,但是這種寫法多少讓人感覺有些生疏。個人的建議是,在沒有特殊需求的情況下,使用第三種和第四種方式實現單例模式。
