Java中6種單例實現方法

banq發表於2024-04-10

在建立 單例時,我們必須確保僅建立一個物件或僅發生一個類的一個例項化。為了確保這一點,以下常見的事情成為先決條件。

  1. 所有建構函式都需要宣告為“ private”建構函式。 
    • 它防止在類外部使用“new”運算子建立物件。
  2. 需要一個私有常量/變數物件持有者來儲存單例物件;即,需要宣告私有靜態或私有靜態最終類變數。
    • 它儲存單例物件。它充當單例物件的單一引用源
    • 按照慣例,該變數被命名為INSTANCE或instance。
  3. 需要一個靜態方法來允許其他物件訪問單例物件。
    • 此靜態方法也稱為靜態工廠方法,因為它控制類的物件的建立。
    • 按照慣例,該方法被命名為getInstance()。 
    <ul>
    有了這個理解,讓我們更深入地瞭解單例,以下是為類建立單例物件的 6 種方法。
  4. 1.靜態急切單例類
    當我們掌握了所有例項屬性,並希望只有一個物件和一個類來為一組相互關聯的屬性提供結構和行為時,我們可以使用靜態急切單例類。這非常適合應用程式配置和應用程式屬性。

    public class EagerSingleton {
      
      private static final EagerSingleton INSTANCE = new EagerSingleton();

      private EagerSingleton() {}
          
      public static EagerSingleton getInstance() {
        return INSTANCE;
      }

      public static void main(String[] args) {
        EagerSingleton eagerSingleton = EagerSingleton.getInstance();
      }
    }

    單例物件是在JVM中載入類本身時建立的,並分配給INSTANCE常量。getInstance()提供對該常量的訪問。

    雖然屬性的編譯時依賴關係很好,但有時需要執行時依賴關係。
    在這種情況下,我們可以使用靜態塊來例項化單例。

    public class EagerSingleton {

        private static EagerSingleton instance;

        private EagerSingleton(){}

     <font>// static block executed during Class loading<i>
        static {
            try {
                instance = new EagerSingleton();
            } catch (Exception e) {
                throw new RuntimeException(
    "Exception occurred in creating EagerSingleton instance");
            }
        }

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

    單例物件是在 JVM 中載入類本身時建立的,因為所有靜態塊都是在載入時執行的。對例項變數的訪問由 getInstance() 靜態方法提供。

    2.動態懶惰單例類
    單例更適合應用配置和應用屬性。考慮到異構容器建立、物件池建立、層建立、門面建立、輕量級物件建立、每個請求的上下文準備以及會話等:它們都需要動態構建一個單例物件,以實現更好的 "關注分離"。在這種情況下,就需要動態的懶單件。

    public class LazySingleton {

        private static LazySingleton instance;

        private LazySingleton(){}

        public static LazySingleton getInstance() {
            if (instance == null) {
                instance = new LazySingleton();
            }
            return instance;
        }
    }

    只有在呼叫 getInstance() 方法時才會建立單例物件。與靜態急切單例類不同,該類不是執行緒安全的。

    public class LazySingleton {

        private static LazySingleton instance;

        private LazySingleton(){}

        public static synchronized LazySingleton getInstance() {
            if (instance == null) {
                instance = new LazySingleton();
            }
            return instance;
        }

    }

    getInstance() 方法需要同步,以確保在單例物件例項化過程中 getInstance() 方法是執行緒安全的。

    3.動態懶惰改進單件類

    public class LazySingleton {

        private static LazySingleton instance;

        private LazySingleton(){}

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

    }

    與其鎖定整個 getInstance() 方法,我們可以只鎖定具有雙重檢查或雙重檢查鎖定的程式碼塊,以提高效能和執行緒競爭性。

    public class EagerAndLazySingleton {

        private EagerAndLazySingleton(){}

        private static class SingletonHelper {
            private static final EagerAndLazySingleton INSTANCE = new EagerAndLazySingleton();
        }

        public static EagerAndLazySingleton getInstance() {
            return SingletonHelper.INSTANCE;
        }
    }

    只有在呼叫 getInstance() 方法時才會建立單例物件。它是 Java 記憶體安全單例類。它是一個執行緒安全的單例類,可以懶載入。它是使用最廣泛的單例類,也是最值得推薦的單例類。

    儘管效能和安全性都有所提高,但在 Java 中,為一個類建立一個物件的唯一目標受到了記憶體引用、反射和序列化的挑戰。

    • 記憶體引用:在多執行緒環境中,執行緒讀寫的重新排序可能會發生在引用的變數上,而且如果變數未宣告為易失性(volatile),髒物件讀取可能會隨時發生。
    • 反射:透過反射,可以將私有建構函式公開,並建立一個新例項。
    • 序列化:序列化後的例項物件可用於建立同一類的另一個例項。

    所有這些都會影響靜態和動態單例。為了克服這些挑戰,我們需要將例項持有者宣告為易失性,並覆蓋 Java 中所有類的預設父類 Object.class 的 equals()、hashCode() 和 readResolve()。

    4.使用列舉的單例
    如果將列舉用於靜態急切單例,就可以避免記憶體安全性、反射和序列化問題。

    public enum EnumSingleton {
        INSTANCE;
    }

    這些都是變相的靜態急迫單例,是執行緒安全的。在需要靜態急於初始化的單例時,最好選擇列舉。

    5.使用函式和庫的單例
    雖然瞭解單例中的挑戰和注意事項是必須的,但既然可以利用成熟的庫,為什麼還要擔心反射、序列化、執行緒安全和記憶體安全呢?Guava 就是這樣一個廣受歡迎的成熟庫,它處理了許多編寫高效 Java 程式的最佳實踐。

    我有幸使用 Guava 庫解釋了基於供應商的單例物件例項化,從而避免了大量繁重的程式碼行。將函式作為引數傳遞是函數語言程式設計的主要特點。雖然供應商函式提供了一種例項化物件生產者的方法,但在我們的例子中,生產者必須只生產一個物件,並且在一次例項化後應不斷重複返回相同的物件。我們可以對建立的物件進行 memoize/快取。使用 lambdas 定義的函式通常會被懶散地呼叫來例項化物件,而 memoization 技術有助於懶散地呼叫動態單例物件建立。

    import com.google.common.base.Supplier;
    import com.google.common.base.Suppliers;

    public class SupplierSingleton {
        private SupplierSingleton() {}

        private static final Supplier<SupplierSingleton> singletonSupplier = Suppliers.memoize(()-> new SupplierSingleton());

        public static SupplierSingleton getInstance() {
            return singletonSupplier.get();
        }

        public static void main(String[] args) {
            SupplierSingleton supplierSingleton = SupplierSingleton.getInstance();
        }
    }

    函數語言程式設計、供應商函式和 memoization 有助於利用快取機制準備單子。這在我們不需要大量框架部署時最為有用。

    6.使用框架的單例:Spring、Guice
    為什麼還要擔心透過供應商準備物件和維護快取呢?Spring 和 Guice 等框架透過 POJO 物件來提供和維護單例。

    這在企業開發中被大量使用,因為在企業開發中,許多模組都需要有自己的上下文和許多層。每個上下文和每個層都是單例模式的良好候選者。

    import org.springframework.beans.factory.config.ConfigurableBeanFactory;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Scope;

    class SingletonBean { }

    @Configuration
    public class SingletonBeanConfig {

        @Bean
        @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
        public SingletonBean singletonBean() {
            return new SingletonBean();
        }

        public static void main(String[] args) {
            AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SingletonBean.class);
            SingletonBean singletonBean = applicationContext.getBean(SingletonBean.class);
        }
    }

    Spring 是一個非常流行的框架。上下文和依賴注入是 Spring 的核心。

    import com.google.inject.AbstractModule;
    import com.google.inject.Guice;
    import com.google.inject.Injector;

    interface ISingletonBean {}

    class SingletonBean implements  ISingletonBean { }

    public class SingletonBeanConfig extends AbstractModule {

        @Override
        protected void configure() {
            bind(ISingletonBean.class).to(SingletonBean.class);
        }

        public static void main(String[] args) {
            Injector injector = Guice.createInjector(new SingletonBeanConfig());
            SingletonBean singletonBean = injector.getInstance(SingletonBean.class);
        }
    }

    來自 Google 的 Guice 也是一個準備單例物件的框架,是 Spring 的替代品。

    以下是使用 "單子工廠 "利用單子物件的方法。

    • 工廠方法、抽象工廠和構建器與在 JVM 中建立和構建特定物件有關。只要我們設想構建一個有特定需求的物件,就能發現單例的需求。以下是可以檢視和發現單例的其他地方。
    • 原型 或輕量級
    • 物件池
    • facade
    • 分層
    • 上下文和類載入器
    • 快取
    • 橫向關注點和麵向方面的程式設計 AOP

    結論
    當我們為業務問題和非功能性需求約束(如效能、安全性、CPU 和記憶體約束)解決用例時,就會出現模式。給定類的單例物件就是這樣一種模式,對其使用的要求也會落到實處。類的本質是建立多個物件的藍圖,然而,動態異構容器需要準備 "上下文"、"層"、"物件池 "和 "函式物件",這確實促使我們利用宣告全域性可訪問或上下文可訪問的物件。
     

    相關文章