Effective Java - 構造器私有、枚舉和單例

Singleton 是指僅僅被實例化一次的類。Singleton表明了無狀態的對象像是方法或者本質上是惟一的系統組件。使類稱爲Singleton 會使它的客戶端測試變得十分困難。由於不可能給Singleton替換模擬實現。除非實現一個充當其類型的接口java

餓漢式單例

靜態常量

下面有兩種方法實現一個單例,二者都基於保持構造器私有而且導出一個公有的靜態成員提供一個惟訪問該實例的入口。在第一種方法中,這個成員的屬性是final安全

// 提供屬性是公有的、惟一的單例
public class Elvis {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis();
  
  public void leaveTheBuilding();
}

這是一個餓漢式的實現。這個私有的構造器僅僅被調用一次,由於Elvis 是 static final的,因此INSTANCE是一個常量,編譯期間進行初始化,而且值只能被初始化一次,導致INSTANCE不能再指向任意其餘的對象,沒有任何客戶端可以改變這個結果。可是須要注意一點:有特權的客戶端可以使用反射中的AccessibleObject.setAccessible訪問私有的構造器。爲了防護這種攻擊,把構造器修改成在第二次實例化的時候拋出異常。見以下的例子多線程

public class Elvis {

    static boolean flag = false;
    private Elvis(){
        if(flag == false) {
            flag = !flag;
        }
        else {
            throw new RuntimeException("單例模式被侵犯!");
        }
    }

    public static class SingletonHolder {
        private static final Elvis INSTANCE = new Elvis();
    }

    public static Elvis getInstance(){
        return SingletonHolder.INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        Class<Elvis> el = Elvis.class;
        // 得到無參數私有的構造器
        Constructor<Elvis> constructor = el.getDeclaredConstructor();
        // 暴力破解private 私有化
        constructor.setAccessible(true);
        // 生成新的實例
        Elvis elvis = constructor.newInstance();
        Elvis instance = Elvis.getInstance();
        System.out.println(elvis == instance);

    }
}
Exception in thread "main" java.lang.ExceptionInInitializerError
    at effectiveJava.effective03.Elvis.getInstance(Elvis.java:22)
    at effectiveJava.effective03.Elvis.main(Elvis.java:33)
Caused by: java.lang.RuntimeException: 單例模式被侵犯!
    at effectiveJava.effective03.Elvis.<init>(Elvis.java:13)
    at effectiveJava.effective03.Elvis.<init>(Elvis.java:5)
    at effectiveJava.effective03.Elvis$SingletonHolder.<clinit>(Elvis.java:18)
    ... 2 more

註釋掉利用反射獲取私有構造函數的代碼,發現instance實例能夠正常輸出併發

Elvis instance = Elvis.getInstance();
System.out.println(instance);

console: effectiveJava.effective03.Elvis@266474c2函數

在實現Singleton 的第二種方法中,公有的成員是個靜態方法性能

public class ElvisSingleton {

    private static final ElvisSingleton INSTANCE = new ElvisSingleton();
    private ElvisSingleton(){}
    public static ElvisSingleton newInstance(){
        return INSTANCE;
    }
    public void leaveBuilding(){}
    
}

對於靜態方法newInstance來講全部的調用,都會返回一個INSTANCE對象,因此,永遠不會建立其餘ElvisSingleton實例測試

公有屬性最大的優點在於可以很清楚的描述類是單例的:公有的屬性是final的,因此老是可以包含相同的對象引用。第二個優點就是就是比較簡單。ui

靜態代碼塊

靜態代碼塊是靜態常量的變種,就是把靜態常量的初始化放在了靜態代碼塊中解析,初始化。讀者可能對這種方式產生疑惑,請詳見.net

類加載機制 https://blog.csdn.net/ns_code/article/details/17881581

public class ElvisStaticBlock {

    private static final ElvisStaticBlock block;
    static {
        block = new ElvisStaticBlock();
    }

    private ElvisStaticBlock(){}
    public static ElvisStaticBlock newInstance(){
        return block;
    }
}

優勢:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。

缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。若是從始至終從未使用過這個實例,則會形成內存的浪費。

懶漢式單例

與餓漢式對應的就是懶漢式,這二者都是屬於單例模式的應用,懶漢式含有一層懶加載(lazy loading)的概念,也叫作惰性初始化。

public class ElvisLazyLoading {

    private static ElvisLazyLoading instance;
    private ElvisLazyLoading(){}

    public static ElvisLazyLoading newInstance(){
        if(instance == null){
            instance = new ElvisLazyLoading();
        }
        return instance;
    }
}

初始的時候不會對INSTANCE進行初始化,它的默認值是null,在調用newInstance方法時會判斷,若INSTANCE爲null,則會把INSTANCE的引用指向ElvisLazyLoading的構造方法。

這種方式可以實現一個懶加載的思想,可是這種寫法會存在併發問題,因爲多線程各自運行本身的執行路徑,當同時執行到 INSTANCE = new ElvisLazyLoading() 代碼時,各自的線程都認爲本身應該建立一個新的ElvisLazyLoading對象,因此最後的結果可能會存在多個ElvisLazyLoading 實例,因此這種方式不推薦使用

嘗試加鎖

很顯然的,能夠嘗試對newInstance()方法加鎖來避免產生併發問題,可是這種方式不可能,由synchronized加鎖會致使整個方法開銷太大,在碰見相似問題時,應該嘗試換一種方式來解決,而不該該只經過簡單粗暴的加鎖來解決一切併發問題。

public synchronized static ElvisLazyLoading newInstance(){
  if(INSTANCE == null){
    INSTANCE = new ElvisLazyLoading();
  }
  return INSTANCE;
}

同步代碼塊

synchronized關鍵字不只能夠鎖住方法的執行,也能夠對方法中的某一塊代碼進行鎖定,也叫作同步代碼塊

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

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

不要以爲只要加鎖了,就不會存在線程安全問題,線程是Java中很重要的一個課題,須要細細研究。這種同步代碼塊的方式也會存在線程安全問題,當多個線程同時判斷本身的singleton 實例爲null的時候,一樣會建立多個實例。

雙重檢查

Double-Check概念對於多線程開發者來講不會陌生,如代碼中所示,咱們進行了兩次if (instance == null)檢查,這樣就能夠保證線程安全了。這樣,實例化代碼只用執行一次,後面再次訪問時,判斷if (instance == null),直接return實例化對象。

public class ElvisDoubleCheck {

    private static volatile ElvisDoubleCheck instance;
    private ElvisDoubleCheck(){}

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

優勢:線程安全;延遲加載;效率較高。

靜態內部類單例

靜態內部類的單例與餓漢式採用的機制相似,但又有不一樣。二者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不一樣的地方在餓漢式方式是隻要Elvis類被裝載就會實例化,沒有Lazy-Loading的做用,而靜態內部類方式在ElvisStaticInnerClass類被裝載時並不會當即實例化,而是在須要實例化時,調用newInstance方法,纔會裝載SingletonInstance類,從而完成ElvisStaticInnerClass的實例化。

public class ElvisStaticInnerClass {

    private ElvisStaticInnerClass(){}

    private static class SingletonInstance{
        private static final ElvisStaticInnerClass instance = new ElvisStaticInnerClass();
    }

    public static ElvisStaticInnerClass newInstance(){
        return SingletonInstance.instance;
    }
}

優勢:避免了線程不安全,延遲加載,效率高。

枚舉單例

實現Singleton的第四種方法是聲明一個包含單個元素的枚舉類型

public enum  ElvisEnum {

    INSTANCE;

    public void leaveTheBuilding(){}
}

這種方法在功能上與公有域方法類似,但更加簡潔。無償地提供了序列化機制,有效防止屢次實例化,即便在面對複雜的序列化或者反射攻擊的時候。單元素的枚舉類型常常成爲實現Singleton的最佳方法

優勢: 系統內存中該類只存在一個對象,節省了系統資源,對於一些須要頻繁建立銷燬的對象,使用單例模式能夠提升系統性能。

缺點:當想實例化一個單例類的時候,必需要記住使用相應的獲取對象的方法,而不是使用new,可能會給其餘開發人員形成困擾,特別是看不到源碼的時候。

後記

看完本文,你是否對構造器私有、枚舉和單例這個主題有了新的認知呢?

你至少應該瞭解:

  1. 單例模式的幾種寫法及其優缺點分析
  2. 爲何反射可以對私有構造器產生破壞?
  3. 有哪幾種比較好用的線程安全的單例模式?

公衆號提供 優質Java資料 以及CSDN免費下載 權限,歡迎你關注我

參考資料:

如何防止單例模式被JAVA反射攻擊 https://blog.csdn.net/u013256816/article/details/50525335

單例模式的八種寫法比較

《Effective Java》

相關文章
相關標籤/搜索