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

c旋兒發表於2019-07-10

Singleton 是指僅僅被例項化一次的類。Singleton代表了無狀態的物件像是方法或者本質上是唯一的系統元件。使類稱為Singleton 會使它的客戶端測試變得十分困難。因為不可能給Singleton替換模擬實現。除非實現一個充當其型別的介面

餓漢式單例

靜態常量

下面有兩種方法實現一個單例,兩者都基於保持構造器私有並且匯出一個公有的靜態成員提供一個唯訪問該例項的入口。在第一種方法中,這個成員的屬性是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的,所以總是能夠包含相同的物件引用。第二個優勢就是就是比較簡單。

靜態程式碼塊

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

類載入機制 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免費下載 許可權,歡迎你關注我
Effective Java - 構造器私有、列舉和單例

參考資料:

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

單例模式的八種寫法比較

《Effective Java》

相關文章