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


閑話一二

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

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

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

使用場景

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

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

  1. 自動化所有用到的接口,在發送https請求時,都需要包含一個參數sessionId,該參數可以通過登錄webserver的接口獲取,我希望這個sessiondId是唯一的,且只需要獲取一次。
  2. 由於系統的webserver是支持高可用的,即如果一個active webserver掛了,另一個standby webserver就會立即投入工作,此時web host就需要切換。為了支持高可用,我在發送請求時加入了兼容代碼:如果捕獲了連接異常(ConnectException)就會去嘗試switchWebHost。在多線程並發執行測試用例的時候,我希望這個switchWebHost操作只需要執行一次。而如果將整個代碼塊加入synchronized同步,會導致不能同時發送https請求,導致並發量降低。

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

定義

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

經典單例模式

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的開銷。

但是,上述的實現方式很容易會想到存在一個嚴重的缺陷,就是“非線程安全”。當多個線程同時調用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內存管理)
到重構自動化的代碼,到反復測試各種條件下的程序執行情況,到寫完整篇總結,已經花了一整天的時間,雖說花的時間有點多,但是知識的掃盲本身就不是一蹴而就的,尤其基礎的東西理解地深刻我相信對以后的學習肯定是有幫助的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM