消化單例設計模式(Java版本)

Valeria00D 7年前發布 | 13K 次閱讀 設計模式 Java Java開發

設計模式在軟件開發人員中非常流行。設計模式是一套代碼設計經驗的總結。單例模式是Java 創建型設計模式 中的一種。

單例模式的目的是什么?

單例類的目的是為了控制對象的創建,限制對象的數量只能是1。單例只允許有一個入口可以創建這個類的實例。

由于只有一個單例實例,所以單例中任何字段的初始化都應該像靜態字段一樣只發生一次。當我們需要控制一些資源比如數據庫連接或者sokets等時,單例就非常有用。

聽起來好像是一個非常簡單的設計模式,但是當要具體實現的時候,它會帶來許多實踐問題。怎么實現單例模式在開發者之間一直是一個有爭議的話題。在這里我們將討論怎樣創建一個單例類來滿足它的需求:

限制類的創建并且保證在java虛擬機中只有一個該類的實例存在。

我們用java創建一個單例類,并在不同的條件下測試。

使用Java創建一個單例類

為了實現單例類,最簡單的方式就是將該類的構造函數定義為私有方法。

  • 1、餓漢式初始化:

在餓漢模式中,單例類的實例在加載這個類的時候就被創建,這是創建單例類最簡單的方法。

將單例類的 構造函數 定義為私有方法,其他類就不能創建該類的實例。取而代之的是通過我們提供的靜態方法入口(通常命名為 getInstance() )來獲取該類實例。

public class SingletonClass {

    private static volatile SingletonClass sSoleInstance = new SingletonClass();

    //私有構造函數
    private SingletonClass(){}

    public static SingletonClass getInstance() {
        return sSoleInstance;
    }
}

這種方法有一個缺點。這里有可能我們不會使用這個實例,但是單例的實例還是會被創建。如果你的單例類在創建中需要建立同數據庫的鏈接或者創建一個socket時,這可能是一個嚴重的問題。因為這可能會導致內存泄漏。解決辦法是當我們需要使用時再創建單例類的實例。這就是所謂的懶漢式初始化。

  • 2、懶漢式初始化:

餓漢式初始化 不同,這里我們由 getInstance()方法自己 初始化單例類的實例。這個方法會檢查該類是否已經有創建實例?如果有,那么 getInstance() 方法會返回已經創建的實例,否則就在JVM中創建一個該類的實例并返回。這種方法就稱為懶漢式初始化。

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    private SingletonClass(){}  //private constructor.

    public static SingletonClass getInstance(){
        if (sSoleInstance == null){ //if there is no instance available... create new one
            sSoleInstance = new SingletonClass();
        }

        return sSoleInstance;
    }
}

我們知道在Java中如果兩個對象相同,那么他們的哈希值也應該相等。那么我們來測試一下。如果上面的單例實現是正確的,那么下面的測試代碼應該返回相同的哈希值。

public class SingletonTester {
   public static void main(String[] args) {
        //Instance 1
        SingletonClass instance1 = SingletonClass.getInstance();

        //Instance 2
        SingletonClass instance2 = SingletonClass.getInstance();

        //now lets check the hash key.
        System.out.println("Instance 1 hash:" + instance1.hashCode());
        System.out.println("Instance 2 hash:" + instance2.hashCode());  
   }
}

下面是兩個實例哈希值的log輸出。

我們可以看到兩個實例的哈希值是相等的。所以,這意味著以上代碼完美的實現了一個單例。是嗎???? 不是。

如果使用Java的反射API呢?

在上面的單例類中,通過使用反射我們可以創建多個實例。如果你不知道 Java反射API ,Java反射就是在代碼運行時檢查或者修改類的運行時行為的過程。

我們可以在運行過程中將單例類的構造函數的可見性修改為public,從而使用修改后的構造函數來創建新的實例。運行下面的代碼,看看我們的單例是否還能幸存?

public class SingletonTester {
   public static void main(String[] args) {
        //創建第一個實例
        SingletonClass instance1 = SingletonClass.getInstance();

        //使用Java反射API創建第二個實例.
        SingletonClass instance2 = null;
        try {
            Class<SingletonClass> clazz = SingletonClass.class;
            Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
            cons.setAccessible(true);
            instance2 = cons.newInstance();
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }

        //現在來檢查一下兩個實例的哈希值
        System.out.println("Instance 1 hash:" + instance1.hashCode());
        System.out.println("Instance 2 hash:" + instance2.hashCode());
   }
}

這里是兩個實例哈希值的log輸出。

哈希值并不相等

兩個實例的哈希值并不相等。這清晰的說明我們的單例類并不能通過這個測試。

解決方法:

為了阻止因為反射導致的測試失敗,如果構造函數已經被調用過還有其他類再次調用時我們必須拋出一個異常。來更新一下 SingletonClass.java

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    //私有構造方法
    private SingletonClass(){

        //阻止通過反射的API調用.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    } 

    public static SingletonClass getInstance(){
        if (sSoleInstance == null){ //如果還沒可用的實例。。。。創建一個
            sSoleInstance = new SingletonClass();
        }

        return sSoleInstance;
    }
}

我們的單例線程安全嗎?

如果有兩個線程幾乎在同時初始化我們的單例類,會發生什么?我們一起來測試一下下面的代碼,這段代碼中兩線程幾乎同時創建并且都調用 getInstance() 方法。

public class SingletonTester {
   public static void main(String[] args) {
        //Thread 1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonClass instance1 = SingletonClass.getInstance();
                System.out.println("Instance 1 hash:" + instance1.hashCode());
            }
        });

        //Thread 2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonClass instance2 = SingletonClass.getInstance();
                System.out.println("Instance 2 hash:" + instance2.hashCode());
            }
        });

        //start both the threads
        t1.start();
        t2.start();
   }
}

如果你運行這段代碼很多次,你會發現有時兩個線程創建了不同的實例:

哈希值并不相等

這意味著我們的單例是 線程不安全的 。如果兩個線程同時調用我們的 getInstance() 方法,那么 sSoleInstance == null 條件對兩個線程都成立,所以會創建同一個類的兩個實例。這破壞了單例規則。

解決方法:

1.將getInstance()方法定義為synchronized:

我們將getInstance()方法定義為synchronized

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    //private constructor.
    private SingletonClass(){

        //Prevent form the reflection api.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    } 

    public synchronized static SingletonClass getInstance(){
        if (sSoleInstance == null){ //if there is no instance available... create new one
            sSoleInstance = new SingletonClass();
        }

        return sSoleInstance;
    }
}

getInstance() 方法定義為synchronized,那么第二個線程就必須等待第一個線程的 getInstance() 方法運行完。這種方式能夠達到線程安全的目的,但是使用這種方法有一些缺點:

  • 頻繁的鎖導致性能低下
  • 一旦實例初始化完成,沒有必要再進行同步操作

2、雙重檢查鎖方法:

如果我們使用雙重檢查鎖方法來創建單例類則可以解決這個問題。在這種方法中,我們僅僅將實例為null條件下的代碼塊同步執行。所以只有在 sSoleInstance 為null的情況下同步代碼塊才會執行,這樣一旦實例變量初始化成功就不會出現不必要的同步操作。

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    //private constructor.
    private SingletonClass(){

        //Prevent form the reflection api.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    public static SingletonClass getInstance() {
        //Double check locking pattern
        if (sSoleInstance == null) { //Check for the first time

            synchronized (SingletonClass.class) {   //Check for the second time.
              //if there is no instance available... create new one
              if (sSoleInstance == null) sSoleInstance = new SingletonClass();
            }
        }

        return sSoleInstance;
    }
}

3、使用volatile

這個方法表面上看起來很完美,因為你只需要執行一次同步代碼塊,但是在你將 sSoleInstance 變量定義為 volatile 之前測試仍然會失敗。

如果沒有volatile修飾符,就可能會出現另一個線程可以訪問到處于半初始化狀態的_instance變量,但是使用了volatile類型的變量,它能保證:對 volatile 變量 sSoleInstance 的寫操作,不允許和它之前的讀寫操作打亂順序;對 volatile 變量 sSoleInstance 的讀操作,不允許和它之后的讀寫亂序。

public class SingletonClass {

        private static volatile SingletonClass sSoleInstance;

        //private constructor.
        private SingletonClass(){

            //Prevent form the reflection api.
            if (sSoleInstance != null){
                throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
            }
        }

        public static SingletonClass getInstance() {
            //Double check locking pattern
            if (sSoleInstance == null) { //Check for the first time

                synchronized (SingletonClass.class) {   //Check for the second time.
                  //if there is no instance available... create new one
                  if (sSoleInstance == null) sSoleInstance = new SingletonClass();
                }
            }

            return sSoleInstance;
        }
    }

現在我們的單例類是線程安全的。保證單例線程安全是非常重要的,尤其是在Android應用這樣的多線程應用環境。

保證單例序列化安全:

在分布式系統中,我們有時需要在單例類中實現 Serializable 接口。通過實現Serializable可以將它的一些狀態存儲在文件系統中以供后續使用。讓我們來測試一下我們的單例類在序列化和反序列化以后是否能夠只有一個實例?

public class SingletonTester {
   public static void main(String[] args) {

      try {
            SingletonClass instance1 = SingletonClass.getInstance();
            ObjectOutput out = null;

            out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
            out.writeObject(instance1);
            out.close();

            //deserialize from file to object
            ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
            SingletonClass instance2 = (SingletonClass) in.readObject();
            in.close();

            System.out.println("instance1 hashCode=" + instance1.hashCode());
            System.out.println("instance2 hashCode=" + instance2.hashCode());

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
   }
}

哈希值不相等

我們可以看到兩個實例的哈希值并不相等。很顯然還是違反了單例原則。上面描述的序列化單例問題是因為當我們需要反序列化一個單例時,會創建一個新的實例。

為了防止創建新的實例,我們必須提供 readResolve() 方法的實現。 readResolve() 取代了從數據流中讀取對象。這樣就能保證其他類通過序列化和反序列化來創建新的實例。

public class SingletonClass implements Serializable {

    private static volatile SingletonClass sSoleInstance;

    //private constructor.
    private SingletonClass(){

        //Prevent form the reflection api.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    public static SingletonClass getInstance() {
        if (sSoleInstance == null) { //if there is no instance available... create new one
            synchronized (SingletonClass.class) {
                if (sSoleInstance == null) sSoleInstance = new SingletonClass();
            }
        }

        return sSoleInstance;
    }

    //Make singleton from serialize and deserialize operation.
    protected SingletonClass readResolve() {
        return getInstance();
    }
}

結論:

行文至此,我們已經創建了一個線程安全、反射安全的單例類。但是這個單例類仍然不是一個完美的單例。我們還可以通過克隆或者多個類加載來創建多個單例類的實例,從而破壞單例規則。但是在多數應用中,上面的單例實現能夠完美的工作。

 

 

來自:http://www.jianshu.com/p/00b7fb6d5142

 

 本文由用戶 Valeria00D 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!