1. 程式人生 > >設計模式學習(四)——單例模式

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

閒話一二

清明小長假,由於沒有回老家探親,趁著難得的三天假期,可以好好地豐富下自己的知識儲備。今天是第一天,上午花了半天時間看了下單例模式,正好解決了最近手頭自動化測試工作中碰到的困擾,也順便了解了下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到執行緒本地記憶體(本地快取)中,建立一個變數副本,之後執行緒就不再和物件在堆記憶體變數值有任何關係,而是直接修改副本變數的值,在修改完之後的某一個時刻(執行緒退出之前),自動把執行緒變數副本的值回寫到物件在堆中變數。下面這幅流程圖描述了一個共享變數線上程中被使用時其執行緒工作記憶體與主記憶體的互動方式。

執行緒工作記憶體

靜態內部類方式

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

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記憶體管理)
到重構自動化的程式碼,到反覆測試各種條件下的程式執行情況,到寫完整篇總結,已經花了一整天的時間,雖說花的時間有點多,但是知識的掃盲本身就不是一蹴而就的,尤其基礎的東西理解地深刻我相信對以後的學習肯定是有幫助的。

相關推薦

設計模式學習——模式

閒話一二 清明小長假,由於沒有回老家探親,趁著難得的三天假期,可以好好地豐富下自己的知識儲備。今天是第一天,上午花了半天時間看了下單例模式,正好解決了最近手頭自動化測試工作中碰到的困擾,也順便了解了下volatile關鍵字的使用。 也許有人會說,網上關於設計模式的文章很多,為什麼還要寫設計模式。但是,那畢竟是

設計模式---模式

由於 div 懶漢式 其它 ins class single sin pub 1、簡介   作為對象的創建模式,單例模式確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例,這個類稱為單例類 2、單例模式有以下三個特點   2.1、單例類只能有一個實例   

Java 設計模式系列模式

重要 理解 iat 版本 ide 默認 ces 內部實現 成功 Java 設計模式系列(五)單例模式 單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。 一、懶漢式單例 /** * 懶漢式單例類.在第一次調用的時候實例化自己 * 1. 構造器私

Java 多執行緒—— 模式

這篇部落格介紹執行緒安全的應用——單例模式。 單例模式   單例模式,是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類只有一個例項。即一個類只有一個物件例項。 例項: /** * @author

HeadFirst 設計模式筆記—— 模式

singleton pattern 確保一個類只有一個實體,並提供一個全域性訪問點。這就是單例模式的功能。 典型的實現: public class Singleton { private static Singleto

設計模式】—-1模式建立型

概念:  java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這裡主要介紹三種:懶漢式單例、餓漢式單例、登記式單例。  單例模式有以下特點:  1、單例類只能有一個例項。  2、單例類必須自己建立自己的唯一例項。  3、單例類必須給所有其他物件提供這一例項。 

設計模式學習總結2模式、建造者模式、原型模式

單例模式(Singleton Pattern) 這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種訪問其唯一的物件的方式,可以直接訪問,不需要例項化該類的物件。 單例模式有以下三點注意: 1、單例類只能有一個例項。 2、單

【原】從頭學習設計模式——模式

一、引入   單例模式作為23種設計模式中的最基礎的一種模式,在平時開發中應用也非常普遍。到底哪些類應該設計成單例的呢,我們來舉個最通俗的例子。在一個父容器中單擊某個選單項開啟一個子視窗,如果不使用單例又沒有作選單項的可用控制的話,每次單擊選單項都會開啟一個新視窗。這不僅會浪費記憶體資源,在程式邏輯

設計模式學習筆記模式

單例模式(Singleton Pattern): Ensure a class has only one instance, and provide a global point of access to it. (確保某一個類只有一個例項,並向整個系統提供這個例項的

java 設計模式 學習筆記16 模式

單例模式:保證一個類僅有一個例項,並提供一個訪問其例項的一個全域性訪問點 根據單例模式的定義,寫一個單例模式的例子需要注意兩點: 1.例項有該類自己生成      為了防止客戶程式碼通過 new Singleton()來例項一個物件,需要將 Singleton的預設建構函式

設計模式4——模式學習及其六大戰將

單例模式的引發的陳年回憶   記著N年以前,那時候還在上大學,有一門科目叫做軟體體系結構,教我們的老師是個40歲左右的女老師,姓韓,好像是東北大學博士畢業的,之所以對她還有些印象,那是因為初戀女友Y同學是被保送到東北大學讀研的,而當時韓老師和Y同學的師生關係好像還挺不錯,當然也是Y同學確定保送東北大學讀研的那

23種設計模式1-模式

如果 static 拓展 銷毀 jvm垃圾收集器 單例類 不為 全局 返回值 定義: 單例模式,是一種常用的軟件設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中一個類只有一個實例。即一個類只有一個對象實例。 特點:

設計模式1----模式

內存 內部 gin strong 系統 方法 main 重新 out 簡介: 單例模式(Singleton)是一種常用的軟件設計模式,在它的核心結構中只包含一個被稱為單例的特殊類。 定義: 確保一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。 單例模式 單例模式

設計模式模式

分享 公有 交互 線程 加鎖 解決 操作 編譯 進入 1.單例模式(Singleton):由於某種需要,要保證一個類在程序的生命周期中只有一個實例,並提供一個該實例的全局訪問方法。 2.單例模式(Singleton)結構圖: Singleton類,定義了一個GetInst

設計模式1——模式

虛擬 span 源碼 加載 定義 實例變量 不能 內存 而且 在Java開發過程中,很多場景下都會碰到或要用到單例模式,在設計模式裏也是經常作為指導學習的熱門模式之一,相信每位開發同事都用到過。我們總是沿著前輩的足跡去做設定好的思路,往往沒去探究為何這麽做,所以這篇文章對單

跟著別人學設計模式-----模式詳解

    作者:zuoxiaolong8810(左瀟龍),轉載自:http://www.cnblogs.com/zuoxiaolong/p/pattern2.html             上一章

常用的軟體設計模式模式

單例模式:即一個類只有一個例項,在類裡自己建立自己的例項。 優點: (1)單例模式會阻止其他物件例項化其自己的單例物件的副本,從而確保所有物件都訪問唯一例項。 (2)因為類控制了例項化過程,所以類可以靈活更改例項化過程。尤其是在C++中,每次new都要delete,而是用單例模式可以避免

設計模式5模式

模式介紹 單例模式是一種建立型設計模式,其中一個類只保證只有一個例項,該例項可以全域性訪問。 這意味著該模式強制特定物件不具有可訪問的建構函式,並且對該物件執行的任何訪問都是在該物件的同一例項上執行的。 示例 我們模擬一下餐館裡用於通知上菜鈴鐺,它在櫃檯上只有一個。 下面程式碼中syncRoot是為

設計模式模式詳解

愉快的開始自己的技術積累 ,設計模式中使用最普遍的單例模式開始; 設計模式中最為大家熟悉的必須是單例模式,專案中 必須 使用到的套路。首先陳述下我對 框架,模式的一些理解。   從學校出來,開始面試的時候張口框架,閉口模式,真的問道什麼是框架,我只能死鴨子嘴硬的強調 MVC ,

PHP設計模式——模式Singleton Pattern

PHP設計模式(一)——單例模式(Singleton Pattern) 單例模式(Singleton Pattern):顧名思義,就是隻有一個例項。作為物件的建立模式,單例模式確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。 (一)為