設計模式學習(四)——單例模式

閒話一二

清明小長假,因爲沒有回老家探親,趁着可貴的三天假期,能夠好好地豐富下本身的知識儲備。今天是第一天,上午花了半天時間看了下單例模式,正好解決了最近手頭自動化測試工做中碰到的困擾,也順便了解了下volatile關鍵字的使用。html

也許有人會說,網上關於設計模式的文章不少,爲何還要寫設計模式。可是,那畢竟是人家的,沒有通過本身的理解、實踐、總結、沉澱,是很難化爲己用的。至於我寫博客的目的,更不是爲了博得他人的關注和承認,主要是爲了將本身學習過的知識能加深理解,吸取前人的優秀經驗和巧妙設計思想,在本身平日的工做中看有沒有能夠借鑑的地方。固然,若是能有經驗豐富的人看了個人博客,不論是在學習工做方式上仍是知識內容上給我些許誠懇的提點和意見,本人將感激涕零。我的博客園地址:http://www.cnblogs.com/znicy/java

另外,隨着知識的積累,不少知識在一段時間不接觸後會遺忘,寫博客的一大好處就是隨時能夠找到以前曾經接觸的這一片區域,而且還能夠抓到當時寫博時的思路,很快地回憶起知識的內容。web

使用場景

開始介紹單例模式以前,必需要先描述下使用場景,以及本身在代碼編寫時遇到的痛點。數據庫

在不少時候,有些對象咱們但願在整個程序只有一個實例,如線程池、數據庫鏈接池、緩存、日誌對象、註冊表等。而最近,在個人實際工做中,在編寫接口自動化代碼時就遇到了下列兩種場景:設計模式

  1. 自動化全部用到的接口,在發送https請求時,都須要包含一個參數sessionId,該參數能夠經過登陸webserver的接口獲取,我但願這個sessiondId是惟一的,且只須要獲取一次。
  2. 因爲系統的webserver是支持高可用的,即若是一個active webserver掛了,另外一個standby webserver就會當即投入工做,此時web host就須要切換。爲了支持高可用,我在發送請求時加入了兼容代碼:若是捕獲了鏈接異常(ConnectException)就會去嘗試switchWebHost。在多線程併發執行測試用例的時候,我但願這個switchWebHost操做只須要執行一次。而若是將整個代碼塊加入synchronized同步,會致使不能同時發送https請求,致使併發量下降。

借用單例模式或借鑑其思想就能夠解決上述問題。api

定義

單例模式確保一個類只有一個實例,並提供一個全局訪問點。緩存

經典單例模式

public class Singleton{
    private static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getInstance(){
        if (null==uniqueInstance){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

Singleton類擁有一個靜態變量uniqueInstance來記錄Singleton的惟一實例,注意它的構造函數是private的,這就註定了只有Sinleton類內纔可使用該構造器。在其餘類中咱們沒法經過new Singleton()的方式類獲取一個Singleton的實例,只能經過Singleton.getInstance()的方式獲取。而且因爲uniqueInstance是一個靜態變量,屬於Singleton這個類,因此保證了其惟一性。安全

經典模式有個好處,就是它的對象的實例化只有等到getInstance方法被調用時纔會被jvm加載,若是getInstance始終沒有被調用,jvm就不會生成該實例。若是該對象的實例化須要消耗較多的資源,這種「延遲實例化」的方式能夠減少jvm的開銷。session

可是,上述的實現方式很容易會想到存在一個嚴重的缺陷,就是「非線程安全」。當多個線程同時調用Singleton.getInstance()來獲取實例時,uniqueInstance對象就可能被屢次實例化。最簡單的方式就是經過synchronized關鍵字來實現線程同步:多線程

public static synchronized Singleton getInstance(){
    if (null==uniqueInstance){
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

「急切實例化」方式

在經典單例模式中加入了synchronized關鍵字後,咱們能夠發現整個getInstance方法是線程同步的操做,當一個線程在調用該方法時,其餘全部線程都會被阻塞。若是getInstance方法的執行時間開銷很小,那麼咱們是可使用這種方式的。可是,若是getInstanc方法的執行時間開銷很大,就會極大地下降併發效率。在這種狀況下,能夠考慮將實例化的操做提早到Singleton類加載的時候,即「急切實例化」方式:

public class Singleton{
    private static Singleton uniqueInstance= new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return uniqueInstance;
    }
}

利用這種方式,咱們能夠依賴jvm在加載這個類時立刻建立此惟一的單例,jvm會保證在任何線程訪問uniqueInstance靜態變量以前,必定先建立此實例。

「雙重檢查加鎖」方式

綜合上述兩種方式,爲了平衡實例建立開銷和併發量受限的代價,「雙重檢查加鎖」經過部分同步的方式同時解決了二者的問題。

public class Singleton{
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public Singleton getInstance(){
        if (null == uniqueInstance){
            synchronized (Singleton.class){
                if( null == uniqueInstance){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

能夠看到,這種方式也是將實例化延遲到了getInstance方法被調用時,區別於經典單例模式,該方式引入了「雙重檢查」,在多線程並行執行到同步代碼塊時,會再次判斷uniqueInsance是否爲null,有效防止了屢次實例化的發生。而且這種方式並無對整個getInstance方法加鎖,只是在第一次生成Singleton的惟一實例時進行了一次同步,並無下降程序的併發性。

volatile關鍵字

而對於volatile關鍵字的使用,查閱了《Thinking in Java》,做者的解釋是「volatile定義的域在發生修改後,它會直接寫到主存中,對其餘任務可見」。

用volatile修飾的變量,線程在每次開始使用變量的時候,都會讀取變量修改後的最新的值。可是這並不表明,使用volatile就能夠實現線程同步,它只是在線程「開始使用」該變量時讀取到該變量的最新值。當線程訪問某一個對象時候值的時候,首先經過對象的引用找到對應在堆內存(主存)的變量的值,而後把堆內存變量的具體值load到線程本地內存(本地緩存)中,創建一個變量副本,以後線程就再也不和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,在修改完以後的某一個時刻(線程退出以前),自動把線程變量副本的值回寫到對象在堆中變量。下面這幅流程圖描述了一個共享變量在線程中被使用時其線程工做內存與主內存的交互方式。

線程工做內存

圖片轉自博客:God is Coder

靜態內部類方式

最後再介紹一下靜態內部類的方式也能夠實現同時知足性能和併發要求的單例模式。

public class Singleton{
    private static class Holder{
       private static Singleton INSTANCE = new Singleton();
    }
    private Singleton(){}
    public static final Singleton getInstance(){
        return Holder.INSTANCE;
    }
}

能夠看到,該方式實際上是第二種「急切實例化」方式的變種,該實例只有在jvm加載類Holder時會被實例化,而且能夠保證在各個線程獲取Holder.INSTANCE變量以前完成。在保證線程安全的同時,又能夠延遲實例化,而且沒有下降併發性。

問題解決

在介紹了幾種單例模式後,如今來解決咱們在「使用場景」中碰到的兩個問題。

1.session獲取

使用「靜態內部類」方法建立SessionFactory類:

public class SessionFactory {
    private static String sessionId;
    private static BaseConfig baseConfig = BaseConfigFactory.getInstance();
    
    private static class SessionidHolder{
        private final static SessionFactory INSTANCE = new SessionFactory();
    }

    public static final SessionFactory getInstance(){
        return SessionidHolder.INSTANCE;
    }
    private SessionFactory(){
        LoginApi api  = new LoginApi();
        String username = baseConfig.getLoginUsername();
        String password = baseConfig.getLoginPassword();
        sessionId= api.login(username, password).getValue("session.id");
    }
    
    public String getSessionId() {
        return sessionId;
    }
}

使用Testng編寫測試代碼:

public class SessionTest {
    @Test(threadPoolSize=10, invocationCount=10)
    public void sessionTest(){
        SessionFactory sessionFactory = SessionFactory.getInstance();
        System.out.println("Thread id="+ Thread.currentThread().getId()+ 
        ", session.id=" + sessionFactory.getSessionId());
    }
}

測試結果:

Thread id=13, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=18, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=11, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=16, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=12, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=17, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=10, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=15, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=14, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
Thread id=19, session.id=36afe1a1-19df-4400-8fbf-4687293d7294

能夠看到,10個線程併發執行時,session.id是惟一的,說明sessionFactory是惟一的,只被實例化了一次。

或許你會問,能不能在SessionFactory中將getSessionId方法設爲靜態方法,直接調用SessionFactory.getSessionId()來獲取sessionId?固然能夠,可是前提是你仍是必須要先經過調用SessionFactory.getInstance()方法來將SessionFactory類實例化,不然你會發現獲取到的sessionId就是null,能夠看出,jvm在加載一個類時,若是該類沒有被實例化就不會去主動調用它的構造方法。

2.遇到webserver切換時,但願switchWebHost操做只須要執行一次

借用「雙重檢查,部分同步」的思想,能夠設計僞代碼邏輯以下(篇幅考慮使用僞代碼代替):

try {
    sendHttpsRequest();
}catch(ConnectException e){
    numRquestFail++;
    synchronized (BaseApi.class) {
        if (isWebHostChanged()){
            return;
        }
        switchWebHost();
    }
}

即,將切換webhost部分的代碼進行同步,而且在切換時先經過調用isWebHostChanged()方法判斷是否已經被其餘線程切換。防止host屢次發生切換。同時,這種方式不會影響到sendHttpsRequest方法的併發。

總結

其實,寫到這裏,從早上開始拿起手頭的《Head First 設計模式》看單例模式,到翻書查資料理解相關的知識(volatile、jvm內存管理) 到重構自動化的代碼,到反覆測試各類條件下的程序執行狀況,到寫完整篇總結,已經花了一成天的時間,雖然說花的時間有點多,可是知識的掃盲自己就不是一蹴而就的,尤爲基礎的東西理解地深入我相信對之後的學習確定是有幫助的。

相關文章
相關標籤/搜索