單例的實現方法總結

單例的實現方法總結

以下的內容不涉及基礎,比如什麼是單例?JVM類加載順序?等等。

僅僅是對所有單例的實現方法進行彙總。

一、最經典的餓漢模式實現方式

public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();
    private Singleton1(){
    }
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

另外一個變種的實現方法,是將靜態成員改爲靜態代碼塊

public class Singleton1_2 {
    private static Singleton1_2 instance;
    static {
        instance = new Singleton1_2();
    }
    private Singleton1_2 (){}
    public static Singleton1_2 getInstance() {
        return instance;
    }
}

不管怎麼寫,本質上利用的都是“類的初始化過程(包含靜態成員賦值,以及靜態代碼塊的執行),只在類被加載到內存時執行一次”這一特性。

  1. 優點

  • 由於其原理,天然就是線程安全的
  • 結構簡單理解容易
  1. 缺點

  • 相對於懶漢模式,餓漢模式最麻煩的地方在於,如果創建單例類的對象要依賴參數或者外部配置文件的話,也就是說,業務場景需要在調用getInstance方法時傳入參數,決定用何種方式創建單例實例的話,餓漢模式就無法使用了。

二、懶漢模式實現方法

public class Singleton2 {
    private static Singleton2 instance;
    private Singleton2 (){
    }
    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}
  1. 優點

  • 在必須使用延遲加載的場景下,替代餓漢模式
  1. 缺點

  • 最重要的一點,爲了確保線程安全,必須使用synchronized關鍵字進行同步,影響性能。

三、雙重檢查方法

雙重檢查其實就是對於懶漢模式的一種性能改進,減小了synchronized關鍵字鎖定的代碼塊範圍。
第二重檢查的作用是:防止有別的線程,在第一重檢查和拿鎖之間創建了單例實例。

public class Singleton3 {
    private volatile static Singleton3 instance;
    private Singleton3 (){
    }
    public static Singleton3 getSingleton() {
        if (instance == null) {
            synchronized (Singleton3.class) {
                if (instance == null) {
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

四、靜態內部類方法

這種方法,也是利用了類加載的特性,在getInstance()方法調用靜態內部類的靜態成員變量時,靜態內部類SingletonHolder纔會被初始化,創建單例實例。
(複習:使用 Class.staticMember 方式引用類的靜態成員變量,屬於對類進行主動引用,在這種情況下會觸發類加載的初始化過程)

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

優點

  • 延遲加載
  • 無鎖,沒有性能損耗
  • 天然線程安全

五、枚舉類

這是一種最簡潔但是最難理解的單例實現方法。但是《Effective Java》評價這是實現單例的最佳方法(參看該書第3條)

public enum Singleton5 {
    /**
     * 枚舉實現單例
     */
    INSTANCE;
    public void businessMethod() {
    }
}  

其調用方法如下:

Singleton5.INSTANCE.businessMethod()

下面解釋枚舉類爲什麼能實現單例。

  1. 首先對於Singleton5編譯好的class進行反編譯

因爲enum只是一個關鍵字,不是超類或者其他能看到源碼的東西。因此利用反編譯的手段來確認內部實現(利用jad)。

package singleton;


public final class Singleton5 extends Enum
{

    public static Singleton5[] values()
    {
        return (Singleton5[])$VALUES.clone();
    }

    public static Singleton5 valueOf(String name)
    {
        return (Singleton5)Enum.valueOf(singleton/Singleton5, name);
    }

    private Singleton5(String s, int i)
    {
        super(s, i);
    }

    public void businessMethod()
    {
    }

    public static final Singleton5 INSTANCE;
    private static final Singleton5 $VALUES[];

    static 
    {
        INSTANCE = new Singleton5("INSTANCE", 0);
        $VALUES = (new Singleton5[] {
            INSTANCE
        });
    }
}

  1. 枚舉如何保證線程安全

可以看到,使用enum關鍵字的話,實際會生成一個繼承了Enum,並且final的類。

public final class Singleton5 extends Enum

注意下面的這一段:

    public static final Singleton5 INSTANCE;
    private static final Singleton5 $VALUES[];

    static 
    {
        INSTANCE = new Singleton5("INSTANCE", 0);
        $VALUES = (new Singleton5[] {
            INSTANCE
        });
    }
  • 根據類加載過程,在“鏈接”的“準備”階段,靜態且final的靜態成員INSTANCE被加載到了方法區(如果這裏有賦值操作的話就有值了,不會等到初始化階段,這是final與其他不同的地方)。
  • 到了初始化階段,會執行靜態代碼塊內的內容,開闢內存空間存放單例實例,並將地址賦值給INSTANCE。
  • 類加載過程是隻會執行一次的,所以本質上還是利用jvm規定的類加載過程,形成了天然的線程安全
  • 另外,構造函數 new Singleton5(“INSTANCE”, 0) 實際上調用是 super,也就是 Enum 類的構造函數,第一個參數是枚舉名稱(name),第二個參數是順序(ordinal)。
  1. 解決反序列化問題

  • 前面除了枚舉類以外的單例的實現方法,都有一個弱點,如果需要進行序列化的話(implements Serializable),那麼在反序列化的時候,每次調用readObject()方法都會生成一個不同於原單例的新實例,單例失效。
  • 對於枚舉,爲了保證枚舉類型符合Java相關規範(JSR),每一個枚舉類及其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java有特殊處理
  • 序列化的時候,Java僅將枚舉類的name屬性輸出到結果中,反序列化的時候通過Enum的valueOf方法來根據名字查找枚舉對象。
    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

  • 而valueOf()調用enumConstantDirectory(),繼而調用getEnumConstantsShared(),可以看到裏面實際調用的是反編譯出來的那段代碼裏的value()方法,使用的實際上就是那個$VALUES[]。
    Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    T[] getEnumConstantsShared() {
        if (enumConstants == null) {
            if (!isEnum()) return null;
            try {
                final Method values = getMethod("values");
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
                        public Void run() {
                                values.setAccessible(true);
                                return null;
                            }
                        });
                @SuppressWarnings("unchecked")
                T[] temporaryConstants = (T[])values.invoke(null);
                enumConstants = temporaryConstants;
            }
            // These can happen when users concoct enum-like classes
            // that don't comply with the enum spec.
            catch (InvocationTargetException | NoSuchMethodException |
                   IllegalAccessException ex) { return null; }
        }
        return enumConstants;
    }
  • 所以枚舉即使被反序列化也不會創建對象。
  • 所以枚舉即使被反序列化也不會創建對象。
  • 所以枚舉即使被反序列化也不會創建對象。

六、應對多個類加載器

前面的所有方法都有一個共通的問題:被多個類加載器加載。

這問題不算是鑽牛角尖,一些熱啓動機制的框架,就是利用多個類加載器實現的,這時候確實有可能造成單例變成多例。

在網上找到了一段代碼來解決這個問題,就是增加下面這個私有靜態類。

原理是在被調用getClass方法時,直接利用自身原來的類加載器進行類加載,確保自始至終一直是同一個類加載器在加載單例類。

private static Class getClass(String classname) throws ClassNotFoundException {     
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
      
      if(classLoader == null)     
         classLoader = Singleton.class.getClassLoader();     
      
      return (classLoader.loadClass(classname));     
   }     
}  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章