1. 程式人生 > >單例模式-最簡單的設計模式?

單例模式-最簡單的設計模式?

一.說在前面

在系統開發設計中,總會存在這麼幾種情況,①需要頻繁建立銷燬的物件,②建立物件需要消耗很多資源,但又經常用到的物件(如工具類物件,頻繁訪問資料庫或檔案的物件,資料來源,session工廠等);③某個類只能有一個物件,如應用中的Application類;這時就應該考慮使用單例模式。個人部落格地址www.mycookies.cn

二.單例模式的動機

  • 在軟體系統中,經常有一些特殊的類,必須保證他們在系統中只存在一個例項,才能保證他們的邏輯正確性,以及良好的效率
  • 如何繞過常規的構造器,提供以中機制保證一個類只有一個例項?這應該是類設計的責任,而不是使用者的責任

三.模式定義

確保類只有一個例項,並提供全域性訪問點。

餓漢式

在類初始化完成之後就完成了物件建立, 無論是否使用都要提前進行例項化。

/**
 * 餓漢式[工廠方法]
 *
 * @author Jann Lee
 * @date 2019-07-21 14:28
 */
public class Singleton1 {

    private static final Singleton1 instance = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return instance;
    }
}

 /**
 * 餓漢式[公有域]
 */
public class Singleton2 {
    
    public static final Singleton2 instance = new Singleton2();
    
    private Singleton2() {
    }
}
 /**
 * 餓漢式[靜態程式碼塊,工廠方法]
 */
public class Singleton3 {

    private static Singleton3 instance;

    static {
        instance = new Singleton3();
    }

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        return instance;
    }
}

以上三種寫法僅僅是在程式碼實現上的差異,是“餓漢式”最常見的實現。

優點:類裝在時候完成例項化,避免了多執行緒問題
缺點:可能造成記憶體浪費(可能例項從來沒有被使用到)

懶漢式

顧名思義,因為懶,所以在用到時才會進行例項化。

實現思路:私有化構造方法-> 宣告成員變數 -> 提供公共方法訪問【如果成員變數不為空,直接返回,如果為空建立後返回】

/**
 * 1.懶漢式[非執行緒安全]
 *
 * @author Jann Lee
 * @date 2019-07-21 14:31
 **/
public class Singleton1 {

    private static Singleton1 instance;

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        if (instance == null) {
            instance = new Singleton1();
        }
        return instance;
    }
}

上述實現方式是最容易被想到的,但是應該也算是一種錯誤的實現方式,因為再多執行緒環境下一個物件可能被建立了多次。

為了解決執行緒安全問題, 將方法進行同步

/**
 * 2.懶漢式[同步方法]
 **/
public class Singleton2 {

    private static Singleton2 instance;

    private Singleton2() {
    }
    
   /**
     * 同步方法,保證執行緒安全
     */
    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

的確,這樣實現解決了併發情況下帶來的問題。但是每次獲取物件都需要進行同步,然而物件建立只需要進行一次即可;因為併發讀取資料不需要進行同步,所以這種在方法進行同步是沒有必要的,在併發情況下肯定會帶來效能上的損失。

這時我們首先想到的時縮小同步範圍,只有在建立物件的時候使用同步,即使用同步程式碼塊實現。

/**
 * 4.懶漢式[同步程式碼塊]
 **/
public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() {
    }

    /**
     * 同步程式碼塊,並不能保證執行緒安全
     */
    public static Singleton3 getInstance() {
        if (instance == null) {
            synchronized (Singleton3.class) {
                instance = new Singleton3();
            }
        }
        return instance;
    }
}

這是一種錯誤的實現方式,雖然減少了加鎖範圍,但是又回到了併發環境下的重複建立物件的問題,具體分析思路可以參考上文中的圖。兩個執行緒在if (instance==null)之後發生了競爭,執行緒1獲取到了鎖,執行緒2掛起,執行完畢後釋放鎖,此時執行緒二獲取到鎖後回接著往下執行,再次建立物件。

解決上述問題,我們只需要在同步程式碼塊中加入校驗。

/**
 * 懶漢式[雙重檢查鎖]
 **/
public class Singleton4 {

    private static volatile Singleton4 instance;

    private Singleton4() {
    }

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

注意:此處除了再次校驗是否為空之外,還給成員變數之前加上了一個volatile關鍵字,這個非常重要。因為在程式碼執行過程中會發生指令重排序。這裡你只需要知道加上volatile之後能保證指令不會被重排序,程式能正確執行,而不加則可能不出錯。

在編譯器和處理器為了提高程式的執行效能,會對指令進行重新排序。即程式碼會被編譯成操作指令,而指令執行順序可能會發生變化。

java中建立物件分為 三個步驟【可以簡單理解為三條指令】

  1. 分配記憶體,記憶體空間初始化
  2. 物件初始化,類的元資料資訊,hashCode等資訊
  3. 將記憶體地址返回

如果2,3順序發生了變化,另一個執行緒獲得鎖時恰好還沒有完成物件初始化,即instance指向null,就會重複建立物件。

靜態內部類

在靜態內部類中持有一個物件。

/**
 * 使用靜態內部類實現單例模式
 *
 * @author Jann Lee
 * @date 2019-07-21 14:47
 **/
public class Singleton {

    private Singleton (){}

    public static Singleton getInstance() {
        return ClassHolder.singleton;
    }

    /**
     * Singleton裝載完成後,不會建立物件
     * 呼叫getInstance時候,靜態內部類ClassHolder才進行裝載
     */
    private static class ClassHolder {
       private static final Singleton singleton = new Singleton();
    }
}

靜態內部類的實現則是根據java語言特性實現的,即讓靜態內部類持有一個物件;根據類載入機機制的特點,每個類只會載入一次,並且只有呼叫getInstance方法時才會載入內部類。這樣就能保證物件只被建立一次,無論是否在多執行緒環境中。

只包含單個元素的列舉型別

在java中列舉也是類的一種,實際上列舉的每一個元素都是一個列舉類的物件,可以理解為對類的一種封裝,預設私有構造方法,且不能使用public修飾。

/**
 * 列舉實現單例模式
 *
 * 為了便於理解給列舉類添加了兩個屬性
 * @author Jann Lee
 * @date 2019-07-21 14:55
 **/
public enum Singleton {

    INSTANCE;

    private String name;

    private int age;

    Singleton04() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

四.要點總結

  • SIngleton模式中的例項構造器可以設定為protected以允許字類派生
  • Singleton模式一般不要支援拷貝建構函式和clone介面,因為這有可能導致多個物件例項,與Singleton模式的初衷違背。
  • 如何實現多執行緒環境下安全的Singleton,注意對雙重檢查鎖的正確實現。

五.思考

  1. java中建立物件的方式有很多種,其中當然包括反射,反序列化,那麼上述各種設計模式還能保證物件只會被建立一次嗎?(這個問題會在下一篇 中進行分析)
  2. volatile關鍵字是一個非常重要的關鍵字,它有那些功能?