設計模式——單例模式

單例模式 (Singleton Pattern)使用的比較多,比如我們的 controller 和 service 都是單例的,但是其和標準的單例模式是有區別的。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

<!--more-->

模式結構

單例模式的結構很簡單,只涉及到一個單例類,這個單例類的構造方法是私有的,該類自身定義了一個靜態私有實例,並向外提供一個靜態的公有函數用於創建或獲取該靜態私有實例。

源碼導讀

單例模式分爲懶漢單例和餓漢單例;餓漢單例代碼很簡單,顧名思義,餓漢單例就是類初始化的時候就將該單例創建,示例代碼如下:

public class Singleton {
    private static final Singleton singleton = new Singleton();
    //限制產生多個對象
    private Singleton(){    
    }
    //通過該方法獲得實例對象
    public static Singleton getSingleton(){
        return singleton;
    }
    //類中其他方法,儘量是 static
    public static void doSomething(){
    }
}

但是懶漢單例就不那麼簡單了,懶漢單例是在訪問這個類的實例的時候先判斷這個類的實例是否創建好了,如果沒創建好就要先創建這個單例。也就是說懶漢單例是第一次訪問的的時候創建單例,而不是初始化階段。這將會導致一個問題,如果在多線程場景下,多個線程同時訪問這個單例都發現其未被創建,那麼這些線程就會分別創建實例,那麼這個單例模式就不那麼單例了——實例被多次創建。在阿里開發手冊中有兩條就是和懶漢單例相關的,告訴我們要如何去避免這種情況,第六節的第一條 和第十二條:

(六)併發處理

1.【強制】獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。

說明:資源驅動類、工具類、單例工廠類都需要注意。

  1. 【推薦】在併發場景下,通過雙重檢查鎖(double-checked locking)實現延遲初始化的優

化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration),推薦解

決方案中較爲簡單一種(適用於 JDK5 及以上版本),將目標屬性聲明爲 volatile 型。

反例:

class Singleton {  
    private Helper helper = null;  
    public Helper getHelper() {  
        if (helper == null) synchronized(this) {  
            if (helper == null)  
            helper = new Helper();  
        }  
        return helper;  
    }  
// other methods and fields...  

} 

volatile 關鍵字的作用和雙重檢查鎖在我以往的博客中介紹過,文章地址https://mp.weixin.qq.com/s/r52hmD71TtiJjlOzQUvRlA 這篇博客介紹了併發的一些知識,小夥伴有空可以讀一讀。在這裏 volatile 關鍵字的作用就是保證數據的可見性,雙重檢查鎖是提高代碼性能。下面我們分析一下手冊中的反例:

其中它的雙重檢測鎖指的是這段代碼:

if (helper == null) synchronized(this) {  
            if (helper == null)  
            helper = new Helper();  
        }  

這裏如果不用雙重檢測鎖的話只能在整個 getHelper 方法上上鎖,因爲這個方法必須要保證在併發情況下只有一個線程會執行helper = new Helper(); ,這段代碼。也就是說代碼 會成爲這樣:

public synchronized Helper getHelper() {  
        if (helper == null)  {  
            if (helper == null)  
            helper = new Helper();  
        }  
        return helper;  
}  

整個方法上鎖性能明顯是不好的,鎖的粒度變大了;雙重檢查鎖裏面爲什麼要做兩次 if 判斷呢,這個問題留給讀者思考,並不是特別難的問題。但是反例裏面沒有考慮到可見性的問題——假設a線程和b線程同時訪問 getHelper 方法,然後 b 線程被阻塞住,a線程發現helper 未被實例化,於是執行new方法,然後釋放鎖;此時b線程進來,或許我們直觀的感受是b線程發現屬性被實例化直接返回helper,但實際上不是,當一個線程修改了線程共享的公共資源的時候(此處是helper屬性)其他線程未必會被通知到屬性被修改,因此b線程有可能發現 helper 還是null 也有可能b線程知道 helper 被賦值了。使用volatile 就可以避免這種情況的發生。因此正確的代碼應該是這樣的:

class Singleton {  
    private volatile Helper helper = null;  
    public Helper getHelper() {  
        ······
    }  
// other methods and fields...  
} 

擴展

單例模式到這裏算是講完了,我再擴展一下單例相關的知識點——問:service 和 controller 都是單例的,它們的代碼也沒有鎖相關的東西,爲什麼是線程安全的?

如果你jvm模型理解的還算透徹的話,這個問題就很好回答。通俗的說就是 service 或者 controller 裏面都是方法,沒有基本數據類型和字符串這樣的屬性。用專業術語回答就是:它們都是無狀態的bean。其實bean的概念是在ejb規範裏面提出來的,後面就被沿用了。感興趣的小夥伴可以去查查資料,瞭解一下ejb規範裏面的三種類型的bean。這裏說一下什麼是無狀態的bean,什麼是bean的狀態。

有狀態就是有數據存儲功能。有狀態對象(Stateful Bean),就是有實例變量的對象,可以保存數據,是非線程安全的。在不同方法調用間不保留任何狀態。無狀態就是一次操作,不能保存數據。無狀態對象(Stateless Bean),就是沒有實例變量的對象.不能保存數據,是不變類,是線程安全的。其
六個核彈

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