搜尋
首頁Javajava教程Java 實作單例的困難點

Java 實作單例的困難點

Dec 05, 2016 am 11:34 AM
java

有簡單又高效的方法可以實現單例模式,但沒有一種方式能在任何情況下都確保單例的完整性。

單例模式是指某個類別只被實例化一次,用來表示全域或系統範圍的元件。單例模式常用於日誌記錄、工廠、視窗管理器和平台元件管理等。我認為要盡量避免使用單例模式,因為一旦實作就很難改變或重載,而且會造成編寫測試案例困難、程式碼結構糟糕等問題。另外,下面文章中的單例模式是不安全的。

人們花大量的精力研究怎樣更好地實現單例模式,但有一種簡單高效的實現方法。然而,沒有一種方法能在任何情況下確保單例的完整性。閱讀下文,看看你是否認同。

Final欄位

這種方法將建構子私有化,向外提供一個公有的static final物件:

public class FooSingleton {    
    public final static FooSingleton INSTANCE = new FooSingleton();   
    private FooSingleton() { }    
    public void bar() { }}

類別載入時,static物件被初始化,此時私有的建構子被第一次也是最後一次調用。即使在類別初始化前有多個執行緒呼叫此類,JVM也能保證執行緒繼續運行時該類別已完整初始化。然而,使用反射和setAccessible(true)方法,可以創建其他新的實例:

Constructor[] constructors = FooSingleton.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton spuriousFoo = (FooSingleton) constructor.newInstance(new Object[0]);

我們需要修改構造函數,使其免於多次調用,例如當它被再次調用時拋出異常。如下這樣修改FooSingleton建構函數,可以防範這類攻擊:

public class FooSingleton2 {    
private static boolean INSTANCE_CREATED;    
public final static FooSingleton2 INSTANCE = new FooSingleton2();    
private FooSingleton2() {        
    if (INSTANCE_CREATED) {            
        throw new IllegalStateException("You must only create one instance of this class");        
     } else {            
         INSTANCE_CREATED = true;        
     }    
  }    
      public void bar() { }
}

這樣看起來安全一些了,但其實要創建新的實例還是一樣容易。我們只需修改INSTANCE_CREATED字段,再玩同樣的把戲就可以了:

Field f = FooSingleton2.class.getDeclaredField("INSTANCE_CREATED");
f.setAccessible(true);
f.set(null, false);
Constructor[] constructors = FooSingleton2.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton2 spuriousFoo = (FooSingleton2) constructor.newInstance(new Object[0]);

我們採取的任何防範措施都可能被繞過,所以此方案並不可行。

靜態工廠

使用這種方法,公有的成員類似靜態工廠:

public class FooSingleton3 {    
  public final static FooSingleton3 INSTANCE = new FooSingleton3();    
  private FooSingleton3() { }    
  public static FooSingleton3 getInstance() { return INSTANCE; }    
  public void bar() { }
}

getInstance()方法返回的永遠是同一個物件參考。雖然這個方案也無法防範反射,但還是有它的一些優點。例如,可以在不改變API的情況下,改變單例的實作。 getInstance()出現在幾乎所有的單例實作中,它也標誌著這真的是一個單例模式。

延遲加載的單例模式

(譯者註:在軟體工程中,Initialization-on-demand holder 這個習語指的就是延遲加載的單例模式,參見維基百科)

如果希望盡可能延遲單例的建立(懶漢式載入),可以使用延遲初始化方法,當getInstance()方法第一次呼叫時執行緒安全地建立單例。相較於之前的方案當第一次引用該類別時就建立單例(餓漢式載入),這是一個進步。如下:

public class FooSingleton4 {    
private FooSingleton4() {    
}    
public static FooSingleton4 getInstance() {       
 return FooSingleton4Holder.INSTANCE;    
 }    
private static class FooSingleton4Holder {       
 private static final FooSingleton4 INSTANCE = new FooSingleton4();    
 }
}

要小心序列化

如果單例實現了序列化,它就要面臨另一個威脅。因此需要將所有欄位宣告為transient(這樣它就不會被序列化)並提供一個自訂的readResolve()方法傳回唯一實例INSTANCE的參考。

枚舉

這裡用枚舉作為單例INSTANCE的容器:

public enum FooEnumSingleton {
    INSTANCE;    
    public static FooEnumSingleton getInstance() { 
    return INSTANCE; 
    }    
    public void bar() { }
}

根據Java語言規範8.9,「Enum的final克隆方法保證枚舉永遠無法被克隆,其特殊的序列化機制保證無法反序列化得到拷貝的物件。我們似乎很簡單地就防範了序列化、克隆和反射的攻擊。第一次看到這段話,我立刻想要證明它是錯的。如下程式碼所示,繞過這些保護是很容易的:

Constructor con = FooEnumSingleton.class.getDeclaredConstructors()[0];
 Method[] methods = con.getClass().getDeclaredMethods();
 for (Method method : methods) {
     if (method.getName().equals("acquireConstructorAccessor")) {
         method.setAccessible(true);
         method.invoke(con, new Object[0]);
     }
  }
  Field[] fields = con.getClass().getDeclaredFields();
  Object ca = null;  for (Field field : fields) {
      if (field.getName().equals("constructorAccessor")) {
          field.setAccessible(true);
          ca = field.get(con);
      }
  }  Method method = ca.getClass().getMethod("newInstance", new Class[]{Object[].class});
  method.setAccessible(true);
  FooEnumSingleton spuriousEnum = (FooEnumSingleton) method.invoke(ca, new Object[]{new Object[]{"SPURIOUS_INSTANCE", 1}});
  printInfo(FooEnumSingleton.INSTANCE);
  printInfo(spuriousEnum);
}private static void printInfo(FooEnumSingleton e) {
    System.out.println(e.getClass() + ":" + e.name() + ":" + e.ordinal());
}

執行這段程式碼,得到結果:

class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:INSTANCE:0
class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:SPURIOUS_INSTANCE:1

枚舉的缺點是它無法從另一個基類繼承,因為它已經繼承自java.lang .Enum。如果想要模擬這種繼承,可以參考我另一篇文章中介紹的混入模式(mixin pattern)。

枚舉的一個優點是,如果你之後希望有“二例(dualton)”或“三例(tringleton)”,只需要增加新的枚舉實例即可。例如,有了一個單例的快取之後,你也許還想給快取引入多個層次。

結論

儘管繞過單例的這些保護並不容易,但確實沒有一種萬無一失的方案。如果你有更好的方案,請不吝賜教!

枚舉是實現單例模式的簡單而又高效的方法。如果想要有繼承或懶漢式加載,延遲初始化方案是不錯的選擇。

祝你的單例好運!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用