單例模式詳細探究

有些東西學完就忘,怎麼辦呢?
還能咋辦,忘了再學唄!

前言

  我們爲什麼需要單例模式呢?每次都new一個對象不行嗎?
  根據我這個快要畢業的、沒有開發經驗的菜鳥來看,內存是有數的,並不是無限的,所以單例模式是爲了儘可能的節約資源,只有一個對象和頻繁的創建對象,孰優孰略想想就知道。

餓漢式

  概念就是很神奇,一眼下去是個懵逼的詞,等你弄懂了又發現這個詞確實還挺形象準確。
  怎麼實現單例?肯定不能隨便讓你new一個對象,把構造器私有化。那怎麼獲取對象呢?自然是暴露出來一個方法能夠獲取這個對象。首先我們來看這個餓漢式的實現方式。什麼是餓漢式呢?就是當類加載完畢,對象就創建好了,不管你用不用,先給你創建上。我們看代碼:

public class TaskManageHungry {
    
    private static TaskManageHungry taskManageHungry = new TaskManageHungry();
    //構造器私有
    private TaskManageHungry() {
        System.out.println("執行構造器===>");
    }
    //獲取這個對象的方法
    public static TaskManageHungry getInstance(){
        return taskManageHungry;
    }
    //測試這個單例模式
	public static void main(String[] args) {
        TaskManageHungry taskManageHungry1 = TaskManageHungry.getInstance();
        TaskManageHungry taskManageHungry2 = TaskManageHungry.getInstance();
        TaskManageHungry taskManageHungry3 = TaskManageHungry.getInstance();
        System.out.println(taskManageHungry1);
        System.out.println(taskManageHungry2);
        System.out.println(taskManageHungry3);
    }
}

這樣我們就實現了這個餓漢式的單例模式,每次都是同一個對象,我們可以運行看看。
在這裏插入圖片描述
  但是呢,這樣雖然實現了單例模式,可它還是有點問題。怎麼說呢,我們先回歸一下單例模式的初心——節約資源。那假如這個對象我用的次數並不多,而且系統啓動後很長時間我才能用得上這個對象,那它直接創建出來,很長時間不被使用,被它佔用的那點內存不就浪費了嗎?所以這就又引出了懶漢式單例

懶漢式

  什麼是懶漢式呢?就是我並不急着創建這個對象,只有當它第一次被調用的時候再創建這個對象。我們還是直接看代碼

public class TaskManageLazy {

    private TaskManageLazy(){
        System.out.println("懶漢式構造器執行");
    }

    private static TaskManageLazy taskManageLazy;

    //獲取實例
    public static TaskManageLazy getInstance(){
        if (taskManageLazy == null) {
            taskManageLazy = new TaskManageLazy();
        }
        return taskManageLazy;
    }

}

  我們在獲取對象的方法里加一個判斷,這樣就實現了懶漢式單例。沒人用的時候,它就是個null,有人調用了,纔會創建對象。
  但是這樣又出現了新的問題,比如我有十個線程,同時調用這個對象,十個線程都是第一次調用,那豈不是就又創建了好幾個對象,單例模式就被破壞了,我們來看看這個多線程情況下會出現什麼樣的結果。

public static void main(String[] args) {
    //測試多線程下
    for (int i = 0; i < 10; i++) {
        new Thread(() -> System.out.println(TaskManageLazy.getInstance())).start();
    }
}

開十個線程來調用,我們來看運行結果。
在這裏插入圖片描述

這說明我們的懶漢式單例還是不夠完善,那怎麼才能在多線程情況下也能實現單例模式呢?當然是上鎖。

public class TaskManageLazy {

    private TaskManageLazy(){
        System.out.println("懶漢式構造器執行");
    }

    private static TaskManageLazy taskManageLazy;

    //獲取實例
    public static TaskManageLazy getInstance(){
        synchronized (TaskManageLazy.class) {
            if (taskManageLazy == null) {
                taskManageLazy = new TaskManageLazy();
            }
        }
        return taskManageLazy;
    }
}

這樣鎖起來確實可以保證線程安全,但是我們想一下,只有第一次執行的時候才需要加鎖來創建對象。如果要是這樣寫,那我每次獲取實例都要鎖起來,不管有沒有對象都要鎖起來,假如我有一百個線程要獲取這個對象,那這一百個線程豈不是要排隊等待着一個個獲取。所以,我們還要把它改造一下,沒有對象的時候加鎖,有對象了就直接返回。

public class TaskManageLazy {

    private TaskManageLazy(){
        System.out.println("懶漢式構造器執行");
    }

    private static TaskManageLazy taskManageLazy;

    //獲取實例
    public static TaskManageLazy getInstance(){
        if (taskManageLazy == null){
            synchronized (TaskManageLazy.class) {
                if (taskManageLazy == null) {
                    taskManageLazy = new TaskManageLazy();
                }
            }
        }
        return taskManageLazy;
    }
}

這種方式就叫做雙重檢查鎖。檢查了兩次null可不就是雙重嘛。
  瞭解到這裏其實也還行,不過我們能不能更深入一點呢?這樣就真的安全了嗎?假如有人用反射破壞了這個單例怎麼辦呢?有沒有一種辦法一勞永逸呢?
  這些事情,就以後再聊吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章