Java單例模式


再孬再好,就你一個

單例模式

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

注意:

  • 單例類限制類的實例個數,保證類的實例在JVM的世界里只有一個類的實例對象。

  • 單例類必須自己創建自己的唯一實例。
  • 單例類必須提供一個全局性的公共訪問方式獲取單例類的實例對象

  • 單例類必須給所有其他對象提供這一實例。

介紹

意圖:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

主要解決:一個全局使用的類頻繁地創建與銷毀。

何時使用:當您想控制實例數目,節省系統資源的時候。

如何解決:判斷系統是否已經有這個單例,如果有則返回,如果沒有則創建。

關鍵代碼:構造函數是私有的。

應用場景: java.lang.Runtime類、日志、驅動器、緩存、線程池等l

實現

我們將創建一個 SingleObject 類。SingleObject 類有它的私有構造函數和本身的一個靜態實例。

SingleObject 類提供了一個靜態方法,供外界獲取它的靜態實例。SingletonPatternDemo,我們的演示類使用 SingleObject 類來獲取 SingleObject 對象。

步驟 1

創建一個 Singleton 類,SingleObject.java

public class SingleObject { //創建 SingleObject 的一個對象 private static SingleObject instance = new SingleObject(0); //讓構造函數為 private,這樣該類就不會被實例化 private SingleObject(int i){ System.out.println("我是一個單例"+i); } //獲取唯一可用的對象 public static SingleObject getSingletonInstance(){ return instance; } public void showMessage(String msg){ System.out.println("我是一個單例的方法調用,"+msg); } }

步驟 2

從 singleton 類獲取唯一的對象,測試類SingletonPatternDemo.java

/** * * @author dgm * @describe "測試單例模式" * @date 2020年5月13日 */ public class SingletonPatternDemo { public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { // 不合法的構造函數 // 編譯時錯誤:構造函數 SingleObject() 是不可見的 // SingleObject object = new SingleObject(); // 獲取唯一可用的對象 SingleObject object = SingleObject.getSingletonInstance(); // 顯示消息 object.showMessage("正常流程"); } }

輸出效果:

 

不過java實現單例模式有好幾種方式,簡單繪圖一張https://www.processon.com/view/link/5ebb9c087d9c08156c3be39a,如下:

我就一一列舉:

1.  餓漢式初始化 Eager initialization

在餓漢初始化模式下,單例類的實例是在類加載的時候完成了創建,這是最簡單的創建單例的方式,但它有個缺陷就是客戶端還沒使用已創建好的單例實例。下面就是實現了靜態初始化的單例類

/** * * @author dgm * @describe "餓漢式初始化單例" */ public class EagerInitializedSingleton { private static final EagerInitializedSingleton instance = new EagerInitializedSingleton(); //注意是私有構造器,防止客戶端使用 private EagerInitializedSingleton(){} //全局訪問方式獲取單實例 public static EagerInitializedSingleton getSingletonInstance(){ return instance; } public void showMessage(String msg){ System.out.println("我是一個餓漢式初始化單例的方法調用,"+msg); } }

 

2. 靜態塊初始化 Static block initialization 

 靜態塊初始化的實現單例和餓漢式初始化類似,但它在創建類實例的時候提供了異常處理

/** * * @author dgm * @describe "靜態代碼塊初始化" */ public class StaticBlockSingleton { private static StaticBlockSingleton instance; private StaticBlockSingleton(){} //在靜態代碼塊初始化進行異常處理 static{ try{ instance = new StaticBlockSingleton(); }catch(Exception e){ throw new RuntimeException("創建單實例時發生了異常"); } } public static StaticBlockSingleton getSingletonInstance(){ return instance; } public void showMessage(String msg){ System.out.println("我是一個靜態代碼塊初始化單例的方法調用,"+msg); } }

3. 延遲初始化 Lazy Initialization

延遲初始化意味着創建單例的是時機不是在類加載過程了,而是在全局訪問方法里實現單例模式的實例創建,就是需要時再創建

/** * * @author dgm * @describe "延遲初始化實例" */ public class LazyInitializedSingleton { private static LazyInitializedSingleton instance; private LazyInitializedSingleton(){} public static LazyInitializedSingleton getSingletonInstance(){ if(instance == null){ instance = new LazyInitializedSingleton(); } return instance; } public void showMessage(String msg){ System.out.println("我是一個延遲初始化單例的方法調用,"+msg); } } 

此種實現方式在單線程環境下表現很好,但是現實中往往是多線程環境,故有了下面的其他實現方式

4.  線程安全型單例Thread Safe Singleton 

有一種簡單創建線程安全的單例,就是給全局訪問方法加上關鍵字synchronized (鎖的一種),它可以確保只有一個線程執行獲取單例的全局訪問方法。

/** * * @author dgm * @describe "線程安全的單例" */ public class ThreadSafeSingleton { private static ThreadSafeSingleton instance; private ThreadSafeSingleton(){} public static synchronized ThreadSafeSingleton getSingletonInstance(){ if(instance == null){ instance = new ThreadSafeSingleton(); } return instance; } public void showMessage(String msg){ System.out.println("我是一個線程安全的單例的方法調用,"+msg); } }

 Above implementation works fine and provides thread-safety but it reduces the performance because of the cost associated with the synchronized method, although we need it only for the first few threads who might create the separate instances (Read: Java Synchronization). To avoid this extra overhead every time, double checked locking principle is used. In this approach, the synchronized block is used inside the if condition with an additional check to ensure that only one instance of a singleton class is created.

該實現確實實現了線程安全的單例,可同步方法對性能有影響,可改造為雙重檢測鎖

/** * * @author dgm * @describe "線程安全雙重檢測的單例" */ public class ThreadSafeDoubleCheckSingleton { private static ThreadSafeDoubleCheckSingleton instance; private ThreadSafeDoubleCheckSingleton(){} public static ThreadSafeDoubleCheckSingleton getSingletonInstance(){ if(instance == null){ synchronized (ThreadSafeDoubleCheckSingleton.class) { if(instance == null){ instance = new ThreadSafeDoubleCheckSingleton(); } } } return instance; } public void showMessage(String msg){ System.out.println("我是一個線程安全雙重檢測單例的方法調用,"+msg); } }

5. 內部靜態輔助類  Inner Static Helper Class 

 

 借助內部靜態類創建單例

/** * * @author dgm * @describe "內部靜態輔助類" */ public class InnerHelperSingleton { private InnerHelperSingleton(){} private static class SingletonHelper{ private static final InnerHelperSingleton INSTANCE = new InnerHelperSingleton(); } public static InnerHelperSingleton getSingletonInstance(){ return SingletonHelper.INSTANCE; } public void showMessage(String msg){ System.out.println("我是一個內部靜態輔助類的單例的方法調用,"+msg); } } 

特別注意,當單例類被加載的時候,內部輔助類還沒有加載到內存,當有客戶端調用公共訪問getSingletonInstance方法時內部類才被加載,然后創建單例對象。

一句話:私有內部靜態類負責創建單例類的實例對象,且不需要同步機制

7. 序列化單例 Serialization and Singleton

有時在分布式系統中,為了在文件系統中存放單實例狀態,我們需要在單例類中實現序列化接口(Serializable interface),下面是一個實現了序列化接口的單實例類:

import java.io.Serializable; /** * * @author dgm * @describe "序列化單例" */ 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 getSingletonInstance(){ return SingletonHelper.instance; } public void showMessage(String msg){ System.out.println("我是一個序列化單例的方法調用,"+msg); } }

.序列化的單實例進行反序列化時會出現問題,創建了一個新的類實例對象,如下代碼:

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; /** * * @author dgm * @describe "序列化單例測試" */ public class SingletonSerializedTest { public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { SerializedSingleton instanceOne = SerializedSingleton.getSingletonInstance(); ObjectOutput out = new ObjectOutputStream(new FileOutputStream( "SerializedSingleton.ser")); out.writeObject(instanceOne); out.close(); //反序列化對象 ObjectInput in = new ObjectInputStream(new FileInputStream( "SerializedSingleton.ser")); SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject(); in.close(); System.err.println("相等否: "+(instanceOne==instanceTwo)); System.out.println("instanceOne hashCode="+instanceOne.hashCode()); System.out.println("instanceTwo hashCode="+instanceTwo.hashCode()); } }

輸出結果:

 

 可以通過在單實例類的定義中加上

private Object readResolve() throws ObjectStreamException{ //instead of the object we're on, //return the class valiable INSTANCE return SingletonHelper.instance; }

 注意,反序列化會判斷是否有readResolve方法

readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class); /** * Returns true if represented class is serializable or externalizable and * defines a conformant readResolve method. Otherwise, returns false. */ boolean hasReadResolveMethod() { return (readResolveMethod != null); }

 這樣當JVM從內存中反序列化地"組裝"一個新對象時,就會自動調用這個 readResolve方法來返回我們指定好的對象了, 單例模式規則也就得到了保證.

 

此時非常好,JVM世界里只有一個單實例對象

 8. 枚舉單例 Enum Singleton

用枚舉的方式實現單例模式也不失為一種好方法,它可以保證任何一個枚舉值只被實例化一次

/** * * @author dgm * @describe "枚舉單例" */ public enum EnumSingleton { INSTANCE, OTHER_INSTANCE; public void showMessage(String msg){ System.out.println("我是一個枚舉型單例的方法調用,"+msg); } }

測試枚舉型單例代碼:

 

public class EnumSingletonTest { public static void main(String[] args) { // TODO Auto-generated method stub EnumSingleton enumSingleton = EnumSingleton.INSTANCE; EnumSingleton otherEnumSingleton = EnumSingleton.INSTANCE; EnumSingleton secondEnumSingleton = EnumSingleton.OTHER_INSTANCE; System.err.println("枚舉型單例對象相等否: "+(enumSingleton==otherEnumSingleton)); System.out.println("enumSingleton hashCode="+enumSingleton.hashCode()); System.out.println("otherEnumSingleton hashCode="+otherEnumSingleton.hashCode()); System.err.println("枚舉型單例對象相等否: "+(enumSingleton==secondEnumSingleton)); } }

 輸出結果

9. 使用反射 ,魔方

可以使用反射,注意,它和沒有使用readResolve()方法的序列化單例模式類似,會破壞單例模式,JVM的世界里會有多個實例對象

import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * * @author dgm * @describe "反射單例" */ public class ReflectionSingletonTest { public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { // 獲取唯一可用的對象 SingleObject object = SingleObject.getSingletonInstance(); // 顯示消息 object.showMessage("正常流程"); // 獲取Class對象 Class<?> singltonClass = null; SingleObject otherObject = null; try { singltonClass = Class.forName("code.singleton.SingleObject", false, SingletonPatternDemo.class.getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } // 已經確定有Class對象,實際情況需要做判斷類加載是否成功,獲取所有構造器 Constructor<?>[] allConstructors = singltonClass .getDeclaredConstructors();// getConstructors(); System.out.println("所有構造器個數:" + allConstructors.length); for (Constructor constructor : allConstructors) { System.out.println("構造器:" + constructor); if (!constructor.isAccessible()) { System.out.println("我是一個私有構造器"); constructor.setAccessible(true); } // 用獲取到的構造器實例化對象 otherObject = (SingleObject) constructor.newInstance(1); // 通過“參數化”獲取類里的方法 Method method = singltonClass .getMethod("showMessage", String.class); // 執行方法 method.invoke(otherObject, "不正常流程"); } System.err.println(object == otherObject); System.out.println("object hashCode="+object.hashCode()); System.out.println("reflection Object hashCode="+otherObject.hashCode()); } } 

執行輸出結果(好可怕破壞單例模式)

發現兩個實例並不相等,反射很強大,被廣泛用於像Spring、Hibernate、Mybatis等框架中,普通開發禁止使用。加上我的反射一文,從此以后,你可以光明正大的說,私有屬性、私有構造器、私有方法可以訪問兼修改了

 

小結: 單例模式實現多種多樣,實際場景自行把控選擇。代碼已進入github: https://github.com/dongguangming/design-pattern/tree/master/src/code/singleton

 

參考:

0. 單例模式   https://baike.baidu.com/item/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/5946627

1. Java Singleton Design Pattern Best Practices with Examples  https://www.journaldev.com/1377/java-singleton-design-pattern-best-practices-examples

2. 要FQ https://javarevisited.blogspot.com/2012/12/how-to-create-thread-safe-singleton-in-java-example.html

3. double checked locking (同樣要FQ)http://jeremymanson.blogspot.com/2008/05/double-checked-locking.html

4. The "Double-Checked Locking is Broken" Declaration?
https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM