1. 程式人生 > >單例模式(建立型)

單例模式(建立型)

  單例模式的主要作用是保證在java程式中,某個類只有一個例項存在,許多時候整個系統只需要擁有一個的全域性物件,這樣有利於協調系統整體的行為。
  一些管理器和控制器常被設計成單例模式,例如在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例物件統一讀取,然後服務程序中的其他物件在通過這個單例物件獲取這些配置資訊;再比如android中的關於SystemSetting中的各種Manager和Service。
  這種方式簡化了在複雜環境下的系統配置和管理。如果一個物件有可能貫穿整個應用程式,而且起到了全域性統一管理控制的作用,那麼單例模式是一個很好的選擇。
單例模式有很多寫法,下面分別進行介紹。
1. 餓漢模式

public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
        return instance;
    }
}

  從程式碼中可以瞭解到,構造方法設為private,這樣保證了其他類無法例項化此類,然後提供一個靜態的方法經靜態的實力返回給呼叫者。這種模式在類載入的時候就對例項進行建立了,並且例項在整個程式週期都存在。它的好處就是隻在類載入的時候建立一次例項,不會存在多個執行緒建立多個例項的情況,避免了多執行緒同步的問題。缺點就是不管這個類有沒有用到都會被建立例項化,就可能出現記憶體浪費的情況,並且會增加程式啟動的時間。
  這個實現方式適用於單例佔用記憶體較小,初始化時就會被用到的情況下。但是,如果單例佔用的記憶體較大,或者是隻有在某特定的情況下才用到(例如在測試環境下才會使用),這個方式就不合適了。這是就需要用懶漢模式進行延遲載入。
2. 懶漢模式

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton newInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

  這種方式是在需要的時候才去建立的,如果單例已經建立好了,再次去呼叫獲取介面將不會重新建立新的物件。如果某個單例使用次數少,並且建立單例消耗的資源較多,那麼就需要實現單例的按需建立,這是懶漢模式就派上用場了。但是這樣寫有一個隱患,就是沒有考慮到多執行緒安全的問題,因此需要加鎖,如下:

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static synchronized Singleton newInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

  加鎖後看起來解決了多執行緒併發的問題,有實現了延遲載入,但還是存在效能問題,因為synchronized修飾的同步方法比一般方法要慢很多,多次呼叫getInstance(),累計的效能消耗就比較可觀了。那麼我們來看看下面改進的雙重校驗鎖的實現。
3. 雙重校驗鎖

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
        //1
        if (instance == null) {   
            synchronized (Singleton.class) {
                //2
                if (instance == null) {   
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  從程式碼看,大部分情況下,呼叫getInstance()都不會執行到同步程式碼塊,從而提高了程式效能。在同步程式碼塊中加入第二次判空的語句就是為了防止少數的情況的發生:假如兩個執行緒A、B,A執行了if (instance == null)語句,它會認為單例物件沒有建立,此時執行緒切到B也執行了同樣的語句,B也認為單例物件沒有建立,然後兩個執行緒依次執行同步程式碼塊,並分別建立了一個單例物件。
  雙重校驗鎖現在看起來解決了執行緒併發的問題,但還不是萬無一失。
這裡就要提到java中的指令重排優化,即在不改變原語義的情況下,通過調整指令的執行順序讓程式執行的更快。JVM中沒用規定編譯器優化相關的內容,自然JVM就可以自由的進行指令重拍的優化,也可稱為指令亂序。
  這個問題的關鍵在於指令重拍優化的存在,導致初始化Singleton和將物件地址賦給instance欄位的順序是不確定的。在某個執行緒建立單例物件時,在構造方法被呼叫之前,就為該物件分配了記憶體空間並將物件的欄位設定為預設值。此時就可以將分配的記憶體地址賦值給instance欄位了,但是該物件可能還沒有初始化。若緊接著另外一個執行緒來呼叫getInstance(),取到的就是狀態不正確的物件,程式便會出錯。
  上述便是雙重校驗鎖會失效的原因,不過JDK1.5之後版本增加了volatile關鍵字,該關鍵字的一個語義就是禁止指令重排序優化,這樣就會避免上述問題。程式碼如下:

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  
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.可以使用反射強行呼叫私有構造器(如果要便面這種情況,可以修改構造器,讓他在建立第二個例項時拋異常)
  列舉很好的解決了這兩個問題,使用列舉除了執行緒安全和防止反射呼叫構造器之外,還提供了自己序列化機制,防止反序列化的時候建立新的物件。