Java單例設計模式最佳實踐及示例

介紹

Java 單例模式 是《四人幫設計模式》之一,屬於創建型設計模式。從定義上看,它似乎是一種直觀的設計模式,但在實現時,卻引發了很多問題。

在本文中,我們將學習單例設計模式的原則,探索不同的實現方式,以及一些最佳實踐。

單例模式原則

  • 單例模式限制了類的實例化,確保在 Java 虛擬機中只存在一個類的實例。
  • 單例類必須提供一個全局訪問點以獲取類的實例。
  • 單例模式用於日誌記錄、驅動程序對象、緩存和線程池
  • 單例設計模式也被用於其他設計模式,如抽象工廠建造者原型外觀等等。
  • 單例設計模式也被用於核心Java類(例如java.lang.Runtimejava.awt.Desktop)。

Java單例模式實現

要實現單例模式,有不同的方法,但它們都有以下共同概念。

  • 私有構造函數,限制其他類實例化該類。
  • 私有靜態變量,是該類的唯一實例。
  • 公共靜態方法,返回該類的實例,這是外部世界獲取單例類實例的全局訪問點。

在後續的部分,我們將學習不同的單例模式實現方法和與實現相關的設計問題。

1. 渴望初始化

在渴望初始化中,单例类的实例是在类加载时创建的。渴望初始化的缺点是即使客户端应用程序可能不使用该方法,该方法也会被创建。以下是静态初始化单例类的实现:

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // 为了避免客户端应用程序使用构造函数而设置的私有构造函数
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
}

如果您的单例类未使用大量资源,这是使用的方法。但在大多数情况下,单例类是为了资源(如文件系统、数据库连接等)而创建的。我们应该避免实例化,除非客户端调用 getInstance 方法。此外,这种方法不提供任何异常处理选项。

2. 静态块初始化

靜態塊初始化實現與急切初始化類似,唯一的不同在於類的實例是在提供選項進行異常處理的靜態塊中創建的。

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // 用於異常處理的靜態塊初始化
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

急切初始化和靜態塊初始化都在實例被使用之前就已經創建了實例,這不是使用的最佳實踐。

3. 懶初始化

通過懶初始化方法來實現單例模式,可以在全局訪問方法中創建實例。以下是使用此方法創建單例類的示例代碼:

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;
    }
}

在單線程環境下,上述實現方法運作良好,但是當涉及到多線程系統時,如果多個線程同時處於if條件內,就可能會出現問題。這將破壞單例模式,兩個線程將獲得單例類的不同實例。在下一節中,我們將看到創建線程安全單例類的不同方法。

4. Thread Safe Singleton

A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:

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;
    }

}

先前的實現方式運作正常並提供了線程安全性,但由於同步方法的成本,它會降低性能,儘管我們只需要為可能創建單獨實例的前幾個線程使用它。為了避免每次都有這額外的開銷,使用了雙重檢查鎖定原則。在這種方法中,同步塊在if條件內使用,並添加額外的檢查以確保僅創建單例類的一個實例。以下代碼片段提供了雙重檢查鎖定的實現:

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

繼續學習,了解線程安全的單例類

5. Bill Pugh 單例實現

在Java 5之前,Java內存模型存在許多問題,以前的方法在某些場景中失敗,當太多的線程同時嘗試獲取單例類的實例時,會出現問題。因此,Bill Pugh提出了一種不同的方法來創建單例類,使用了一個內部靜態助手類。這裡是Bill Pugh Singleton實現的例子:

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()方法時,這個類才會被加載並創建單例類實例。這是最廣泛使用的單例類方法,因為它不需要同步。

6. 使用反射來破壞單例模式

反射可以用來破壞所有以前的單例實現方法。這裡是一個例子類:

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) {
                // 這段代碼將破壞單例模式
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

當你運行上面的測試類時,你會注意到兩個實例的hashCode不同,這破壞了單例模式。反射非常強大,在許多框架中都被使用,如Spring和Hibernate。繼續你的學習,閱讀Java反射教程

7. 枚舉單例

為了克服這種情況,Joshua Bloch建議使用enum來實現單例設計模式,因為Java確保任何enum值在Java程序中僅實例化一次。由於Java枚舉值是全局可訪問的,因此單例也是如此。缺點是enum類型有些不靈活(例如,它不允許延遲初始化)。

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // 做一些事情
    }
}

8. 序列化和單例

有時候在分散式系統中,我們需要在單例類中實現Serializable接口,以便我們可以將其狀態存儲在文件系統中,並在以後的某個時間點檢索它。這是一個實現了Serializable接口的小型單例類:

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;
    }

}

序列化單例類的問題在於每次對其進行反序列化時,都會創建該類的新實例。這是一個示例:

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();

        // 從文件反序列化為對象
        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());

    }

}

該代碼產生以下輸出:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

因此,它破壞了單例模式。為了克服這種情況,我們只需要提供readResolve()方法的實現。

protected Object readResolve() {
    return getInstance();
}

之後,您將注意到測試程序中兩個實例的hashCode是相同的。

閱讀關於Java序列化Java反序列化的內容。

結論

本文介紹了單例設計模式。

繼續閱讀更多Java教程

Source:
https://www.digitalocean.com/community/tutorials/java-singleton-design-pattern-best-practices-examples