Java中單例設計模式之最佳實踐舉例

單例模式(Singleton)是“四人組”(GoF)設計模式中的一種,歸類於創建型模式。從定義上來看,它似乎是非常簡單的設計模式,但是當去實現它時,卻又帶來了很多實現方面的擔憂。單例模式的實現一直是開發者之間的一個富有爭議的話題。在這裏,我們將瞭解單例設計模式的原則,用不同的方式來實現單例模式以及一些使用上的最佳實踐。

單例模式

單例模式限制了類的實例,並確保在Java虛擬機中有且僅有一個類的實例對象的存在。這個單例類必須提供一個全局的訪問點來獲得這個類的實例。單例模式一般用於日誌類(logging),驅動程序對象,緩存以及線程池(thread pool)中。

單例設計模式也用於其它的設計模式中,如抽象工廠Abstract Factory)模式,創建者模式(Builder),原型模式(Prototype),門面模式(Facade)等等。單例設計模式也用於核心Java類中,例如java.lang.Runtime,java.awt.Desktop.

Java中的單例模式

當我們去實現單例模式時,有各種不同的方法,但所有的方法中都不外乎以下幾點。

  • 私有構造函數,限制從其它類中來進行實例化。
  • 同一個類的私有靜態變量,它的實例對象只有唯一一個。
  • 公有的靜態方法,返回該類的實例對象,這是從外部獲得這個類的實例對象的一個全局訪問點。

在下面,我們將會學習單例模式實現的不同方法和它的設計與實現。
1.餓漢式初始化
2.靜態塊初始化
3.懶漢式初始化
4.單例模式的線程安全問題
5.Bill Pugh式單例模式實現
6.反射技術對單例模式的破壞
7.枚舉單例模式
8.單例模式與序列化

餓漢式初始化

在餓漢式初始化中,單例模式的實例對象在類加載的時候就創建了,這是創建單例模式類的最簡單的方法,但它有一個缺陷,就是當客戶端可能不使用它的時候,實例對象還是會被創建。

下面是靜態初始化單例類的實現。

EagerInitializedSingleton.java
package com.journaldev.singleton;
 
public class EagerInitializedSingleton {
     
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
     
    //private constructor to avoid client applications to use constructor
    private EagerInitializedSingleton(){}
 
    public static EagerInitializedSingleton getInstance(){
        return instance;
    }
}

如果你的單例類不使用大量的資源,可以使用這種方式。但是在大多數情況下,單例類是爲一些比如文件系統,數據庫連接資源等而創建的。這時我們應該避免它的實例化,除非客戶端調用getInstance方法。此外,這種方式的實例化不能提供任何用於異常處理的選項。

靜態塊初始化

靜態塊(Static block)的初始化與餓漢式初始化相似,不同之處在於類的實例化提供了異常處理(exception handling)的選項。

StaticBlockSingleton.java
package com.journaldev.singleton;
 
public class StaticBlockSingleton {
 
    private static StaticBlockSingleton instance;
     
    private StaticBlockSingleton(){}
     
    //static block initialization for exception handling
    static{
        try{
            instance = new StaticBlockSingleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
     
    public static StaticBlockSingleton getInstance(){
        return instance;
    }
}

前面兩種方式的初始化都是在使用之前就已經創建了實例對象,並不是使用單例設計模式的最佳方式。因此在下面的部分中,我們將學習怎樣創建懶漢式的單例類。

知識擴展:Java static

懶漢式初始化

懶漢式初始化實現了在全局的訪問方法中去創建單例模式類的實例對象。下面是用這種方式創建單例類的代碼。

LazyInitializedSingleton.java
package com.journaldev.singleton;
 
public class LazyInitializedSingleton {
 
    private static LazyInitializedSingleton instance;
     
    private LazyInitializedSingleton(){}
     
    public static LazyInitializedSingleton getInstance(){
        if(instance == null){
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

上面這種實現方法在單線程中工作的很好,但是在多線程的環境中,如果多個線程是在同一時間訪問的話,它可能就會導致問題了。它將破壞單例模式,而且兩個線程會得到不同的單例類的實例對象。在下面的部分中,我們將用不同的方式來創建線程安全的單例類。

單例模式的線程安全問題

創建一個線程安全的單例類的一個更簡單的方法是使用全局同步訪問方法,以便每次只有一個線程可以執行這個方法,一般這種方法的實現如下類所示。

ThreadSafeSingleton.java
package com.journaldev.singleton;
 
public class ThreadSafeSingleton {
 
    private static ThreadSafeSingleton instance;
     
    private ThreadSafeSingleton(){}
     
    public static synchronized ThreadSafeSingleton getInstance(){
        if(instance == null){
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
     
}

上面這種方式工作的很好而且是線程安全的,但是它也由於同步方法的成本而降低了程序的性能,儘管我們只需要它在少數幾個線程中可以創建單獨的實例。爲了避免每次都產生這種額外的開銷,我們使用雙重檢測鎖(double checked locking)原則。在這種方法中,同步塊放在if條件中進行檢查,以確保只有唯一一個單例類的實例對象被創建。

下面的代碼片段提供了雙重檢測鎖原則的實現。

public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
    if(instance == null){
        synchronized (ThreadSafeSingleton.class) {
            if(instance == null){
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

知識擴展:Thread Safe Singleton Class

Bill Pugh式單例模式實現

在Java5之前,Java的內存模式有很多的問題,當多個線程同時去獲得單例類的實例對象時,在某些情形下,以上那些方法的訪問可能都會失敗。因此Bill Pugh想出了一個不同的方法來創建單例類,即通過使用靜態內部類。它的實現方式如下:

BillPughSingletong.java

package com.journaldev.singleton;
 
public class BillPughSingleton {
 
    private BillPughSingleton(){}
     
    private static class SingletonHelper{
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
     
    public static BillPughSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}
注意是在私有靜態內部類中包含了單例類的實例對象。但單例類被加載時,SingletonHelper類並沒有被加載到內存中去,僅僅當有人去調用getInstance方法時,這個類才被加載,單例類對象才被創建。

這種單例模式類創建的方法使用的最廣泛,因爲它不需要額外的同步鎖開銷,而且也很容易理解和實現。

知識擴展:Java Nested Classes

反射技術對單例模式的破壞

反射可以用來摧毀上面所有的單例實現方法,讓我們來看看下面這個例子吧。

ReflectionSingletonTest.java

package com.journaldev.singleton;

import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
 
    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                //Below code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }
 
}

當你運行這個測試類的時候,你會發現兩個實例對象的哈希值不一樣,可見單例模式已經被破壞了。反射是一種很強大,並且在框架中如SpringHibernate中使用的很多的技術。更多請查閱Java Reflection Tutorial.

枚舉單例模式

爲了克服上面反射的那種情況,Joshua Bloch建議使用枚舉來實現單例設計模式,因爲Java能確保任何枚舉值在Java程序中僅被實例化一次。由於Java的枚舉(Java Enum)是全局訪問的,所以在單例模式中,使用起來可能不太靈活,比如,它不支持懶漢式初始化。

EnumSingleton.java

package com.journaldev.singleton;
 
public enum EnumSingleton {
 
    INSTANCE;
     
    public static void doSomething(){
        //do something
    }
}

知識擴展:Java Enum

單例模式與序列化

有時在分佈式系統中,我們需要在單例類中實現序列化的接口,以便我們能將它的狀態儲存在文件系統中,而在以後的某個時間點,我們又能將其從文件系統中恢復進行使用。下面是一個很小的實現了序列化接口的單例類。

SerializedSingleton.java

package com.journaldev.singleton;
 
import java.io.Serializable;
 
public class SerializedSingleton implements Serializable{
 
    private static final long serialVersionUID = -7604766932017737115L;
 
    private SerializedSingleton(){}
     
    private static class SingletonHelper{
        private static final SerializedSingleton instance = new SerializedSingleton();
    }
     
    public static SerializedSingleton getInstance(){
        return SingletonHelper.instance;
    }
     
}

這種序列化單例類的一個問題就是當我們反序列化它的時候,它會創建一個新的實例對象。看下面這段簡單的程序。

SingletonSerializedTest.java

package com.journaldev.singleton;
 
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
 
public class SingletonSerializedTest {
 
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();
         
        //deserailize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();
         
        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
         
    }
 
}
上面程序的輸出結果爲:

instanceOne hashCode=2011117821
instanceTwo hashCode=109647522
所以它破壞了單例模式,爲了克服這個問題,我們只需要提供一個實現了的readResolve()方法。
protected Object readResolve() {
    return getInstance();
}
這樣之後,你會發現在測試代碼中兩個實例對象的哈希值是一樣的啦。

知識擴展:Java Serialization and  Java Deserialization.

希望這篇文章能夠幫助你抓住單例設計模式的要點。

想了解更多?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章