Singleton——單例模式(8種)

KaWaniu發表於2021-01-02

單例模式:單例類只建立一個物件,類提供一種訪問其唯一物件的方式,可以直接訪問,不需要例項化該類的物件。

demo:

餓漢式:

靜態常量方式:

public class Singleton {
    //1.構造器私有化,外部不能new
    private Singleton(){}

    //2.本類內部建立物件例項
    private final static Singleton instance=new Singleton();

    //3.提供一個公有的靜態方法,返回例項物件
    public static Singleton getInstance(){
        return instance;
    }
}

靜態程式碼塊方式:

public class Singleton {
    //1.構造器私有化,外部不能new
    private Singleton() {
    }

    //2.本類內部建立物件例項
    private static Singleton instance;

    //靜態程式碼塊隨著類的載入而執行,而且只執行一次
    static {
        //在靜態程式碼塊中建立的物件是單例的
        instance = new Singleton();
    }

    //3.提供一個公有的靜態方法,返回例項物件
    public static Singleton getInstance() {
        return instance;
    }
}

餓漢式單例模式在類裝載的時候完成例項化,避免了執行緒同步問題,但沒有達到懶載入的效果,如果從始至終都沒用過這個實力就會造成記憶體浪費。

 

懶漢式:

執行緒不安全:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    //提供一個靜態的公有方法,用到該方法時才去建立instance
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

將餓漢方式中類裝載時就例項化物件的程式碼放到了靜態方法裡,這樣需要用類物件的時候調靜態方法實現類的例項化,不用的時候就不例項化,達到懶載入的效果,但多執行緒情況下會導致instance=null處的判斷失效,從而使單例失效,所以這種方式是執行緒不安全的

執行緒安全:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    //提供一個靜態的公有方法,用到該方法時才去建立instance,保證懶載入
    //加入同步處理程式碼,解決執行緒安全問題
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        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;
    }
}

 

雙重檢查:

public class Singleton {

    //為什麼用volatile:
    //new物件時:
    //1.分配記憶體空間
    //2.執行構造方法,初始化物件
    //3.把這個物件指向這個空間
    //當new時底層不按123的順序執行時,第一個判斷處顯示!null但物件還沒完成構造,會出錯,所以用volatile:避免指令重排
    private static volatile Singleton instance;

    private Singleton() {
    }

    //提供一個靜態的公有方法,加入雙重檢查程式碼,解決執行緒安全問題,同時解決懶載入問題
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                   instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

這個看註釋就ok了

 

靜態內部類:

public class Singleton {
    //1.構造器私有化,外部不能new
    private Singleton() {
    }

    //2.寫一個靜態內部類,該類中有一個靜態屬性 Singleton
    private static class SingletonInstance {
        private static final Singleton INSTANCE=new Singleton();
    }

    //3.提供一個靜態的公有方法,直接返回SingletonInstance.INSTANCE,synchronized:保證執行緒安全
    public static synchronized Singleton getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

之前掉單例物件的時候都是 類.靜態方法 ,靜態內部類的寫法相當於將呼叫的程式碼做了一層封裝,在單例型別寫個靜態內部類,在單例類中寫個公開的方法呼叫靜態內部類的方法例項化出單例物件,通過執行緒同步機制保證外界呼叫時的執行緒安全;就不用之前的 instance == null 判斷了,執行效率高了點

 

以上單例方式的客戶端呼叫方式:

public class Singleton01 {
    public static void main(String[] args) {
        // 測試
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2);
        System.out.println("instance1.hashCode="+instance1.hashCode());
        System.out.println("instance2.hashCode="+instance2.hashCode());
    }
}

觀察上述7種實現方式可以發現保證單例的核心是構造器私有化,讓外部不能new它,然後在類內部提供一種得到類物件的方法供外界呼叫,單例模式都完美的利用了java的語法特性。但當看到私有化(private)時大家有沒有想到什麼,——“萬惡”的反射,通過反射可以壞private,從而破壞單例,當然這是極端情況,但有就不能疏忽。那說了那麼多那不白撤了嗎,莫慌,來看看列舉是怎麼做的。

 

單例模式——列舉方式:

enum Singleton {

    //利用列舉特性將物件託給列舉
    INSTANCE;

}

客戶端呼叫:

public class Singleton07 {
    public static void main(String[] args) {
        // 測試
        Singleton instance1 = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;
        System.out.println(instance1 == instance2);
        System.out.println("instance1.hashCode=" + instance1.hashCode());
        System.out.println("instance2.hashCode=" + instance2.hashCode());

    }
}

為什麼列舉不能被反射破壞的呢,來看反射的程式碼是怎麼約束反射的:

反射不能例項化列舉類

這下就清楚了吧。

 

總結:對頻繁建立銷燬的物件,使用單例可以提高系統效能;例項化一個單例類時,必須呼叫效應的獲取物件的方法,而不是用new。

單例模式適用情景:建立物件耗時過多或耗資源過多;經常用的物件如工具類;頻繁訪問資料庫或檔案的物件如資料來源、session工廠等

 

相關文章