單例模式--我,我,我,還是我

小熊最近997加班寫代碼,身體嚴重透支。
沒辦法,轉行去賣房子了。

1·懶漢模式

客戶打電話給門店找小熊看房,同事就讓客戶等一下,他去叫小熊。

public class Singleton{
    private static Singleton singleton;
    
    private Singleton() {
    }

    public synchronized static Single newInstance() {
        if (singleton== null) {//小熊在不在
            singleton= new Singleton();//不在就把他找出來
        }
        return singleton;
    }
}

客戶打電話就是一個線程,synchronized就是請客戶等一下,避免還有其他客戶找小熊亂了。

這就是最常見的單例模式,通過newInstance()來延遲加載,synchronized避免多線程問題。

2·餓漢模式

小熊早上9點到了門店。
同事說:有個客戶9點30分要找你,你就在這等着吧!
小熊是新人呢,沒辦法,就等啊等,等得都沒時間去吃早飯,就變成了餓熊~

public class Singleton {
    //小熊就提前等着,不管有沒有人叫他
    private static Singleton singleton= new Singleton();
    
    private singleton(){}

    public static Singleton newInstance() {
    //客戶來電話了,小熊出去帶客戶看房了,可是沒吃早飯好餓呀!
        return singleton;
    }
}

在類加載的時候,最初就聲明瞭對象,避免了多線程問題,可是考慮對象初始化的複雜程度所需要消耗的時間和沒必要的內存開銷,這麼寫也太隨便了。

3·雙重檢查鎖(懶漢模式的升級版)

懶漢模式有個問題呀,就是synchronized影響程序性能呀,每次newInstance()都有synchronized

那就想個方法,我不給方法synchronized,我先判斷有沒有singObj,沒有,我再加個synchronized,然後再判斷一次

public final class DoubleCheckedSingleton
{
    private static DoubleCheckedSingleton singObj = null;

    private DoubleCheckedSingleton(){
    }

    public static DoubleCheckedSingleton getSingleInstance(){
        if(null == singObj ) {                                //第一次檢查
              Synchronized(DoubleCheckedSingleton.class){     //加鎖
                     if(null == singObj)                      //第二次檢查
                           singObj = new DoubleCheckedSingleton();
              }
         }
       return singObj;
    }
}

這裏看似沒有問題,其實我們都忽略了編譯器初始化對象的過程。

singObj = new DoubleCheckedSingleton();

在編譯器中,他的步驟是:
1·分配對象域(內存空間);
2·初始化對象;
3·singObj指向對象域(內存空間);

但是在大多數情況下,編譯器很有可能對語句進行重排序(這種做法是爲了在不改變語句意思的情況下,儘可能減少讀取和存儲的次數,提高效率

那它的步驟就變成了132;

如果AB線程同時去調用getSingleInstance()

編譯器給A線程132的順序創建對象,singObj指向還未完全初始化的對象域;singObj!=null,而對象初始化需要時間;

B線程同時請求的時候,第一次檢查null == singObj返回false,證明有singObj,而singObj指向的對象域中的對象還未完成初始化,這時就發生了不可預知的情況(看到小熊個鬼哦!)

這一切都是編譯器的重排序搞的鬼,該如何解決?

很簡單,我們在聲明singObj的時候添加volatile修飾符。

  • volatile修飾符指明瞭對象之於線程的可見性(一個線程修改對象,另一個線程是可見的,可知的)
  • 另一個作用就是它禁止了編譯器對指令的重排序

在強制編譯器按照123的順序初始化對象後,B線程就會在第一次檢查等待singObj的初始化,兩個線程都會取到同一個對象。

可是前面說了,編譯器的重排序是爲了提高效率,加了volatile後,效率就有所下降;

4·懶加載靜態內部類(推薦

public class Singleton{

    private static class SingletonHolder{ 
        public final static Singleton instance = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

靜態內部類在最初就會被初始化,同時創建了它的內存空間,而其中的對象會在調用的時候被加載(懶加載)

有點類似餓漢模式,只是使用了靜態內部類的方法讓單例變得線程安全。。。

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