設計模式-單例模式

引言

單例模式,這個名字大家絕對是耳熟能詳。作爲對象的創建模式,單例模式實質上是爲了確保某一個類它只有一個實例,並且自行實例化向整個系統提供這個實例。


單例模式的要點

單例模式的要點有三個,這也是我們在設計單例模式時需要注意的幾點:
  • 單例類在整個系統的運行過程中只能有一個實例;
  • 單例類必須要自行來創建實例(換句話說,在Java中就是不對外暴露構造方法,不能由其他的對象來做實例化,除了自己);
  • 單例類必須自行向這個系統提供這個實例。

單例模式的實現


餓漢式單例類

餓漢式單例類是實現起來最爲簡單的單例類,如下類圖所示:

代碼如下:
public class Singleton {
    
    private static final Singleton instance = new Singleton();
    
    private Singleton() {
        
    }
    
    public static Singleton getInstance() {
        return instance;
    }
}
可以看出,在在這個類被加載時,靜態變量instance會被初始化,這時候類的私有構造方法會被調用,單例類的唯一實例被創建。
需要注意的是:由於構造方法是私有的,單例類不能是不能被繼承的。

懶漢式單例類

先看看類圖:

代碼如下:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public synchronized static LazySingleton getInstance() {
        if (null == instance) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
從代碼可以看出,跟餓漢式不一樣的是不是在類被加載的時候就實例化,而是在單例類第一次被引用的時候再實例化。上述代碼還稍微做了一寫措施來保證線程安全。

到這裏已經給出了兩種單例實現的模式,但是細心的讀者可能都會發現,這兩種方式對併發的處理並不是那麼的好,單例模式最需要注意的就是線程安全的問題,因爲全程可能有n個對象都在修改單例類對象。
之前有了解過單例模式或者看過博客的讀者可能很容易就想到double check,雙重檢查,接下來我就懶漢模式從線程不安全到線程安全舉例加以說明,同時說說爲什麼我覺得雙重檢查是錯誤的,做不到線程安全。

線程安全的單例

首先考慮單線程版本的:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        if (null == instance) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
很明顯,在多線程的環境下,這樣子寫你的系統肯定game over了。
我給具體分析下爲什麼這樣寫是錯誤的:
假設現在有A和B兩個線程幾乎同時到底if(null == instance),假設A比B早那麼一點點,那麼:
  1. A首先進入if(null == instance)代碼塊內部,並且開始執行new LazySingleton()語句,此時,instance的值仍然爲null,直到賦值語句執行完畢;
  2. 線程B不會在if(null == instance)語句外等待,此時還未賦值給instance,if語句成立,它會馬上進入if代碼塊內部,B也開始執行instance = new LazySingleton()語句,創建出第二個實例;
  3. A的instance = new LazySingleton()執行完畢後,這時候instance不爲null了,第三個線程不會再進入到if代碼塊內部;
  4. B也創建了一個實例,instance的變量值被覆蓋,但是A引用的之前的instance不會被改變。這時候A和B都各自擁有一個獨立的instance對象,這是不對的。
將上述代碼做個小優化,讓它線程安全:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public synchronized static LazySingleton getInstance() {
        if (null == instance) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
加了synchronized關鍵字之後,這個靜態工廠方法都是同步的,當線程A和B同時調用此方法時:
  1. 早到一點點的線程A率先進入此方法,B在方法外部等待;
  2. A執行instance = new LazySingleton(),創建出實例;
  3. 方法鎖釋放,線程B進入此方法,此時instance不再爲null,if代碼塊不會再次被執行。線程B取到的instance變量所含有的引用與A是同一個。
看到這裏思維比較活躍的可能會覺得其實只需要在instance變量第一次被賦值的時候做好同步就行了,直接在方法上加鎖是不是開銷太大了啊?需要細化鎖的粒度。然後雙重檢查就被提出了,但是,在這裏我有必要提醒一句:雙重檢查在Java編譯器裏無法實現,所以它是不對的!!!接下來我就來分析分析爲什麼雙重檢查做不到線程安全。
先看看雙重檢查代碼:
public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        if (null == instance) { //第一次檢查,位置1
            //多個線程同時到達,位置2
            synchronized(LazySingleton.class) {
                //這裏只能有一個線程,位置3
                if (null == instance) {//第二次檢查,位置4
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

同樣還是假設線程A和B幾乎同事調用靜態工廠方法:
  1. 因爲A和B是一批調用者,因此當它們進入此靜態方法工廠時,instance變量爲null,線程A和B幾乎同時到達位置1;
  2. 假設A先到達位置2,進入synchronized(LazySingleton.class)到達位置3,這是,由於synchronized(LazySingleton.class)的同步限制,B無法到達位置3,只能在位置2等候;
  3. A執行instance = new LazySingleton()語句,實例化instance,此時B還在位置2處等候;
  4. A退出synchronized(LazySingleton.class),返回instance,退出靜態工廠方法;
  5. B進入synchronized(LazySingleton.class)塊,到達位置3,並且到達位置4,這時instance已不爲空,B退出synchronized(LazySingleton.class),返回A創建的instance,退出工廠方法。此時A和B得到的是同一個instance對象。
表面上看,這個方法多麼完美的解決了線程安全問題,並且節省了好多開銷,然而,雙重檢查在Java編譯器中根本就不能成立,爲什麼呢?

雙重檢查成例對Java語言編譯器不成立

在Java編譯器中,LazySingleton類的初始化和instance變量賦值的順序是不可預料的,如果一個線程在沒有同步化的條件下讀取instance的引用,並且調用這個對象的方法的話,可能會發現對象的初始化過程尚未完成,從而造成崩潰。

在一般情況下,餓漢模式+懶漢模式基本上足以解決在實際設計工作中遇到的問題,建議讀者不要過分在雙重檢查上面花費太多時間。




發佈了46 篇原創文章 · 獲贊 10 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章