在建立單例類時,請考慮使用枚舉。它解決了可能會因反序列化和反射而出現的問題。
單例是應該在每個JVM中隻有一個執行個體的類。單例類的Same執行個體被多個線程重用。大多數情況下,我們使用單例來表示系統配置和視窗管理器,因為這些執行個體應該是JVM中所有線程和對象所共有的。【優銳課】抽絲剝繭 細說架構那些事抽絲剝繭 細說架構那些事
制作單例的傳統方法
有幾種流行的制作單例的方法。
方法1:具有公共靜态最終字段的單例public class Singleton {public static final Singleton INSTANCE = new Singleton();private Singleton() {}}
方法2:使用公共靜态工廠方法的單例public class Singleton {private static final Singleton INSTANCE = new Singleton();private Singleton() {}public static Singleton getInstance(){return INSTANCE;}}
方法3:具有延遲初始化的單例public class Singleton {private static Singleton INSTANCE = null;private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {synchronized (Singleton.class) {if (INSTANCE == null) {INSTANCE = new Singleton();}}}return INSTANCE;}}
上述方法的優缺點
以上所有方法都使用私有構造函數強制執行不滿足要求(無法建立執行個體)。在這裡,即使我們沒有任何事要做,我們也無法避免建立私有構造函數。因為如果這樣做,那麼将使用與該類相同的通路修飾符建立一個隐式的無參數預設構造函數。例如,如果将該類聲明為public,則預設構造函數為public否則,預設構造函數為public。如果該類被聲明為protected,則預設構造函數為受保護的(有關更多詳細資訊,請參考Oracle文檔)。
比較上述方法,前兩種方法根本沒有性能差異。方法1更清晰,更簡單。方法2的一個小優點是,以後,你可以使該類成為非單一類,而無需更改API。你可以通過更改factory方法的實作為每個調用建立一個新執行個體,而不是按以下方式傳回相同的執行個體來實作。public static Singleton getInstance() {return new Singleton ();}
靜态字段在類加載時初始化。是以,在方法1和2中,即使在我們在運作時不使用它們的情況下,也會建立單例執行個體。隻要單例對象不是太大,并且建立執行個體也不是太昂貴,就沒有問題。方法3避免了延遲初始化的問題。在方法3中,執行個體是在我們第一次通路單例對象時建立的。細粒度同步用于確定使用多個并發線程建立的對象不超過一個。
在不使用單例類進行序列化和反序列化之前,上述所有方法都可以正常工作。讓我們再想想:我們如何通過上述方法實作單例行為?通過将構造函數設為私有,并使構造函數不可用于建立類的新執行個體來實作。但是除了構造函數之外,沒有其他方法可以建立類的執行個體嗎?答案是不。還有其他一些進階方法。
1.序列化和反序列化
2.反思
序列化和反序列化的問題
為了序列化上述單例類,我們必須使用Serializable接口實作這些類。但是,這樣做還不夠。反序列化類時,将建立新執行個體。現在,構造函數是否私有都無關緊要。現在,JVM中可能有多個同一個單例類的執行個體,這違反了單例屬性。public class SerializeDemo implements Serializable {public static void main(String[] args) {Singleton singleton = Singleton.INSTANCE;singleton.setValue(1);// Serializetry {FileOutputStream fileOut = new FileOutputStream("out.ser");ObjectOutputStream out = new ObjectOutputStream(fileOut);out.writeObject(singleton);out.close();fileOut.close();} catch (IOException e) {e.printStackTrace();}singleton.setValue(2);// DeserializeSingleton singleton2 = null;try {FileInputStream fileIn = new FileInputStream("out.ser");ObjectInputStream in = new ObjectInputStream(fileIn);singleton2 = (Singleton) in.readObject();in.close();fileIn.close();} catch (IOException i) {i.printStackTrace();} catch (ClassNotFoundException c) {System.out.println("singletons.SingletonEnum class not found");c.printStackTrace();}if (singleton == singleton2) {System.out.println("Two objects are same");} else {System.out.println("Two objects are not same");}System.out.println(singleton.getValue());System.out.println(singleton2.getValue());}}
上面代碼的輸出是:Two objects are not same21
在這裡,singleton和singleton2是兩個不同的執行個體,具有兩個不同的值作為其字段變量。這違反了單例屬性。解決方案是我們必須實作readResolve方法,該方法在準備反序列化的對象并将其傳回給調用者之前準備好。解決方法如下。public class Singleton implements Serializable{public static final Singleton INSTANCE = new Singleton();private Singleton() {}protected Object readResolve() {return INSTANCE;}}
現在,以上代碼的輸出将是:Two objects are same22
現在,保留了singleton屬性,并且JVM中僅存在singleton類的一個執行個體。
反思問題
進階使用者可以在運作時使用反射将構造函數的私有通路修飾符更改為所需的任何内容。如果發生這種情況,我們唯一的非不穩定機制就會中斷。讓我們看看如何做到這一點。public class ReflectionDemo {public static void main(String[] args) throws Exception {Singleton singleton = Singleton.INSTANCE;Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);constructor.setAccessible(true);Singleton singleton2 = (Singleton) constructor.newInstance();if (singleton == singleton2) {System.out.println("Two objects are same");} else {System.out.println("Two objects are not same");}singleton.setValue(1);singleton2.setValue(2);System.out.println(singleton.getValue());System.out.println(singleton2.getValue());}}
輸出:Two objects are not same12
這樣,不可通路的私有構造函數變得可通路,并且使類成為單例的整個想法被打破。
在Java中使用枚舉建立單例
通過使用枚舉類型使單例很容易解決所有上述問題。
枚舉單例public enum Singleton {INSTANCE;}
上面的三行代碼構成了一個單例,沒有讨論任何問題。由于枚舉本質上是可序列化的,是以我們不需要使用可序列化的接口來實作它。反射問題也不存在。是以,可以100%保證JVM中僅存在一個單例執行個體。是以,建議将此方法作為Java中制作單例的最佳方法。
如何使用public enum SingletonEnum {INSTANCE;int value;public int getValue() {return value;}public void setValue(int value) {this.value = value;}}
主類實作:public class EnumDemo {public static void main(String[] args) {SingletonEnum singleton = SingletonEnum.INSTANCE;System.out.println(singleton.getValue());singleton.setValue(2);System.out.println(singleton.getValue());}}
這裡要記住的一件事是,當序列化一個枚舉時,字段變量不會被序列化。例如,如果我們對SingletonEnum類進行序列化和反序列化,則将丢失int值字段的值(有關枚舉序列化的更多詳細資訊,請參考Oracle文檔)。