閑話一二

清明小長假,由于沒有回老家探親,趁著難得的三天假期,可以好好地豐富下自己的知識儲備。今天是第一天,上午花了半天時間看了下單例模式,正好解決了最近手頭自動化測試工作中碰到的困擾,也順便了解了下volatile關(guān)鍵字的使用。

也許有人會說,網(wǎng)上關(guān)于設(shè)計模式的文章很多,為什么還要寫設(shè)計模式。但是,那畢竟是人家的,沒有經(jīng)過自己的理解、實踐、總結(jié)、沉淀,是很難化為己用的。至于我寫博客的目的,更不是為了博得他人的關(guān)注和認可,主要是為了將自己學(xué)習(xí)過的知識能加深理解,吸收前人的優(yōu)秀經(jīng)驗和巧妙設(shè)計思想,在自己平日的工作中看有沒有可以借鑒的地方。當然,如果能有經(jīng)驗豐富的人看了我的博客,不管是在學(xué)習(xí)工作方式上還是知識內(nèi)容上給我些許誠懇的提點和意見,本人將感激不盡。個人博客園地址:http://www.cnblogs.com/znicy/

另外,隨著知識的積累,很多知識在一段時間不接觸后會遺忘,寫博客的一大好處就是隨時可以找到之前曾經(jīng)接觸的這一片區(qū)域,并且還可以抓到當時寫博時的思路,很快地回憶起知識的內(nèi)容。

使用場景

開始介紹單例模式之前,必須要先描述下使用場景,以及自己在代碼編寫時遇到的痛點。

在很多時候,有些對象我們希望在整個程序只有一個實例,如線程池、數(shù)據(jù)庫連接池、緩存、日志對象、注冊表等。而最近,在我的實際工作中,在編寫接口自動化代碼時就遇到了下列兩種場景:

  1. 自動化所有用到的接口,在發(fā)送https請求時,都需要包含一個參數(shù)sessionId,該參數(shù)可以通過登錄webserver的接口獲取,我希望這個sessiondId是唯一的,且只需要獲取一次。

  2. 由于系統(tǒng)的webserver是支持高可用的,即如果一個active webserver掛了,另一個standby webserver就會立即投入工作,此時web host就需要切換。為了支持高可用,我在發(fā)送請求時加入了兼容代碼:如果捕獲了連接異常(ConnectException)就會去嘗試switchWebHost。在多線程并發(fā)執(zhí)行測試用例的時候,我希望這個switchWebHost操作只需要執(zhí)行一次。而如果將整個代碼塊加入synchronized同步,會導(dǎo)致不能同時發(fā)送https請求,導(dǎo)致并發(fā)量降低。

借用單例模式或借鑒其思想就可以解決上述問題。

定義

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

經(jīng)典單例模式

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

Singleton類擁有一個靜態(tài)變量uniqueInstance來記錄Singleton的唯一實例,注意它的構(gòu)造函數(shù)是private的,這就注定了只有Sinleton類內(nèi)才可以使用該構(gòu)造器。在其他類中我們無法通過new Singleton()的方式類獲取一個Singleton的實例,只能通過Singleton.getInstance()的方式獲取。并且由于uniqueInstance是一個靜態(tài)變量,屬于Singleton這個類,所以保證了其唯一性。

經(jīng)典模式有個好處,就是它的對象的實例化只有等到getInstance方法被調(diào)用時才會被jvm加載,如果getInstance始終沒有被調(diào)用,jvm就不會生成該實例。如果該對象的實例化需要消耗較多的資源,這種“延遲實例化”的方式可以減小jvm的開銷。

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

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

“急切實例化”方式

在經(jīng)典單例模式中加入了synchronized關(guān)鍵字后,我們可以發(fā)現(xiàn)整個getInstance方法是線程同步的操作,當一個線程在調(diào)用該方法時,其他所有線程都會被阻塞。如果getInstance方法的執(zhí)行時間開銷很小,那么我們是可以使用這種方式的。但是,如果getInstanc方法的執(zhí)行時間開銷很大,就會極大地降低并發(fā)效率。在這種情況下,可以考慮將實例化的操作提前到Singleton類加載的時候,即“急切實例化”方式:

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

利用這種方式,我們可以依賴jvm在加載這個類時馬上創(chuàng)建此唯一的單例,jvm會保證在任何線程訪問uniqueInstance靜態(tài)變量之前,一定先創(chuàng)建此實例。

“雙重檢查加鎖”方式

綜合上述兩種方式,為了平衡實例創(chuàng)建開銷和并發(fā)量受限的代價,“雙重檢查加鎖”通過部分同步的方式同時解決了兩者的問題。

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方法被調(diào)用時,區(qū)別于經(jīng)典單例模式,該方式引入了“雙重檢查”,在多線程并行執(zhí)行到同步代碼塊時,會再次判斷uniqueInsance是否為null,有效防止了多次實例化的發(fā)生。并且這種方式并沒有對整個getInstance方法加鎖,只是在第一次生成Singleton的唯一實例時進行了一次同步,并沒有降低程序的并發(fā)性。

volatile關(guān)鍵字

而對于volatile關(guān)鍵字的使用,查閱了《Thinking in Java》,作者的解釋是“volatile定義的域在發(fā)生修改后,它會直接寫到主存中,對其他任務(wù)可見”。

用volatile修飾的變量,線程在每次開始使用變量的時候,都會讀取變量修改后的最新的值。但是這并不代表,使用volatile就可以實現(xiàn)線程同步,它只是在線程“開始使用”該變量時讀取到該變量的最新值。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應(yīng)在堆內(nèi)存(主存)的變量的值,然后把堆內(nèi)存變量的具體值load到線程本地內(nèi)存(本地緩存)中,建立一個變量副本,之后線程就不再和對象在堆內(nèi)存變量值有任何關(guān)系,而是直接修改副本變量的值,在修改完之后的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。下面這幅流程圖描述了一個共享變量在線程中被使用時其線程工作內(nèi)存與主內(nèi)存的交互方式。

萬碼學(xué)堂,電腦培訓(xùn),計算機培訓(xùn),Java培訓(xùn),JavaEE開發(fā)培訓(xùn),青島軟件培訓(xùn),軟件工程師培訓(xùn)

圖片轉(zhuǎn)自博客:God is Coder

靜態(tài)內(nèi)部類方式

最后再介紹一下靜態(tài)內(nèi)部類的方式也可以實現(xiàn)同時滿足性能和并發(fā)要求的單例模式。

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變量之前完成。在保證線程安全的同時,又可以延遲實例化,并且沒有降低并發(fā)性。

問題解決

在介紹了幾種單例模式后,現(xiàn)在來解決我們在“使用場景”中碰到的兩個問題。

1.session獲取

使用“靜態(tài)內(nèi)部類”方法創(chuàng)建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());
    }
}

測試結(jié)果:

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個線程并發(fā)執(zhí)行時,session.id是唯一的,說明sessionFactory是唯一的,只被實例化了一次。

或許你會問,能不能在SessionFactory中將getSessionId方法設(shè)為靜態(tài)方法,直接調(diào)用SessionFactory.getSessionId()來獲取sessionId?當然可以,但是前提是你還是必須要先通過調(diào)用SessionFactory.getInstance()方法來將SessionFactory類實例化,否則你會發(fā)現(xiàn)獲取到的sessionId就是null,可以看出,jvm在加載一個類時,如果該類沒有被實例化就不會去主動調(diào)用它的構(gòu)造方法。

2.遇到webserver切換時,希望switchWebHost操作只需要執(zhí)行一次

借用“雙重檢查,部分同步”的思想,可以設(shè)計偽代碼邏輯如下(篇幅考慮使用偽代碼代替):

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

即,將切換webhost部分的代碼進行同步,并且在切換時先通過調(diào)用isWebHostChanged()方法判斷是否已經(jīng)被其他線程切換。防止host多次發(fā)生切換。同時,這種方式不會影響到sendHttpsRequest方法的并發(fā)。

總結(jié)

其實,寫到這里,從早上開始拿起手頭的《Head First 設(shè)計模式》看單例模式,到翻書查資料理解相關(guān)的知識(volatile、jvm內(nèi)存管理)
到重構(gòu)自動化的代碼,到反復(fù)測試各種條件下的程序執(zhí)行情況,到寫完整篇總結(jié),已經(jīng)花了一整天的時間,雖說花的時間有點多,但是知識的掃盲本身就不是一蹴而就的,尤其基礎(chǔ)的東西理解地深刻我相信對以后的學(xué)習(xí)肯定是有幫助的。

http://www.cnblogs.com/znicy/p/6659386.html