設計模式詳解(四)----------單例模式

来源:http://www.cnblogs.com/ZhangHaoShuaiGe/archive/2017/11/23/7885413.html
-Advertisement-
Play Games

上一章我們學習了裝飾者模式,這章LZ帶給大家的是單例模式。 首先單例模式是用來幹嘛的?它是用來實例化一個獨一無二的對象!那這有什麼用處?有一些對象我們只需要一個,比如緩存,線程池等。而事實上,這類對象只能有一個示例,如果製造多個示例,就會導致許多問題產生,比如程式的行為異常,資源使用過量。而單例模式 ...


上一章我們學習了裝飾者模式,這章LZ帶給大家的是單例模式。

首先單例模式是用來幹嘛的?它是用來實例化一個獨一無二的對象!那這有什麼用處?有一些對象我們只需要一個,比如緩存,線程池等。而事實上,這類對象只能有一個示例,如果製造多個示例,就會導致許多問題產生,比如程式的行為異常,資源使用過量。而單例模式可以幫助我們確保只有一個實例會被創建。首先我們來看一段代碼:

public class MyClass {
    private static MyClass myClass;
    private MyClass(){
        
    }
    public static MyClass getInstance(){
        if(myClass ==null){
            
            myClass =  new MyClass();
        }
        return myClass;
    }
}

 

1.首先我們創建一個靜態實例,而帶有static關鍵字的屬性在每一個類中都是唯一的。

 2.接著我們將構造方法私有化,從而限制調用者隨意創造實例,這也是保證單例的最重要的一步。

 3.當然,我們必須要給一個可供調用方使用的獲取實例的靜態方法,這裡必須是靜態方法,為什麼呢?請註意,如果我們給的是非靜態的,那麼調用方必須擁有實例才能調用這個方法,但是既然沒有調用這個方法,調用方又哪裡來的實例呢?這不是自相矛盾嗎

4.我們加一個判斷,當只有持有的靜態實例為null時才調用構造方法創造一個實例並把它賦予myClass靜態變數中,註意,如果我們不需要這個實例,它就永遠不會產生,這就是“延遲實例化”。

由此我們可以看出來,單例模式確保一個類只有一個實例,並提供一個全局訪問點。

是不是很簡單?事實上單例模式確實特別簡單,不過LZ還有些內容沒有說完。

如果各位去公司面試,面試官讓你們寫一個單例模式,你們把上面LZ給的代碼寫給面試官,如果你們是應屆生,也許面試官會覺得不錯,但如果你們已經是工作超過一年的同學,那麼寫出上面的代碼恐怕你們就要完蛋。為什麼呢?其實這是一個併發的問題,上面的代碼在不考慮併發的情況下,確實沒有問題,但是一旦考慮多線程併發,就會出現問題。

 下麵LZ用事實說話,給大家模擬一下多線程併發的情況

public class TestMyClass {    
        boolean myLock ;
            
        public boolean isMyLock() {
            return myLock;
        }

        public void setMyLock(boolean myLock) {
            this.myLock = myLock;
        }

        public static void main(String[] args) throws Exception {
            int num=100;
            final CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
            final Set<String> set=Collections.synchronizedSet(new HashSet<String>());
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<num;i++){
                executorService.execute(new Runnable() {
                    public void run() {
                        try {
                            cyclicBarrier.await();
                            MyClass myClass = MyClass.getInstance();
                            set.add(myClass.toString());
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
            Thread.sleep(2000);
            System.out.println("------併發情況下我們取到的實例------");
            for (String instance : set) {
                System.out.println(instance);
            }
            executorService.shutdown();
         
        }
    
}

代碼比較簡單,這裡LZ是用的柵欄阻塞等待所有線程創建完畢,然後同時執行獲取實例的操作。

  LZ在程式中同時開啟了100個線程來訪問getInstance方法,然後把獲得實例的實例字元串裝入同步的set集合,這裡為什麼要放到set集合就不用LZ解釋了吧=。=set集合會自動去重,所以我們看結果輸出了多少實例字元串,就說明我們在併發訪問的過程中產生了多少實例。

               這裡我讓main線程睡眠了一次,是為了給足夠的時間讓100個線程全部開啟。下麵我們看一下結果(如果你照我的代碼演示結果出現了一個,不要驚訝。我試了試大概3次之內就會出現我這種情況,甚至出現4個的都有)

那麼為什麼會造成這種情況呢?

當併發訪問的時候,第一個調用getInstance方法的線程A,在判斷完myClass是null的時候,線程A就進入了if塊準備創造實例,說時遲那時快,在這同時另外一個線程B線上程A還未創造出實例之前,就又進行了myClass是否為null的判斷,這時myClass當然依然為null,所以線程B也會進入if塊去創造實例,那麼問題就出來了,有兩個線程都進入了if塊去創造實例,結果就造成產生了兩個對象出來。接下來LZ做的一個類似於圖的東西,各位可以看看,雖然看起來不太直觀,但是配合LZ的講解詳細各位一目瞭然。

 1 public static MyClass getInstance(){                                            對象的狀態
 2                                         public static MyClass getInstance(){                      null
 3         if(myClass ==null){                                                    null
 4                                                     if(myClass ==null){                         null
 5             myClass =  new MyClass();                                             object1
 6         }
 7         return myClass;                                                      object1
 8                                                         myClass =  new MyClass();                   object2
 9                                                         }
10                                                         return myClass;                         object2
11     }

那麼,我們又應該怎麼解決這個線程併發導致的問題呢?

詳細各位會立刻想起synchronized關鍵字,我們只要把getInstance()變成同步方法,就可以以上的問題了。

public class MyClass {
    private static MyClass myClass;
    private MyClass(){
        
    }
    public synchronized static MyClass getInstance(){
                                
        if(myClass ==null){
            
            myClass =  new MyClass();
        }
        return myClass;
    }
}

通過加上synchronized關鍵字到getInstance()方法前,我們迫使每個線程在進入此方法前,必須先等待其他線程離開,就是說,不會有兩個線程同時進入此方法。

但是,如果我們這樣做,就會導致性能降低,因為,我們只有第一次調用getInstance()這個方法的時候需要同步,而當一旦設置好了myClass這個變數,我們就不需要再同步了,那麼之後我們每次都同步,會導致性能降低。那麼順著這個角度去思考,我們可以先去判斷myClass是否為null,當它為null時再同步。

public class MyClass {
    private static MyClass myClass;
    private MyClass(){
        
    }
    public  static MyClass getInstance(){
                                
        if(myClass ==null){
            synchronized(MyClass.class){
                if(myClass ==null){
                    myClass =  new MyClass();
                }
            }
        }
        return myClass;
    }
}

 

這種做法也被稱為雙重加鎖

  經過剛纔LZ的分析,這種做法應該是滿足了要求,看起來是沒有問題了,但如果我們再進一步深入考慮的話,其實仍然是有可能出現問題的。

   這裡我們深入到JVM中去探索上面這段代碼,相信各位都知道虛擬機在執行創建實例的這一步操作的時候,其實是分了好幾步去進行的,專業點說,創建一個新的對象並非是原子性操作。在有些JVM中上述做法是沒有問題的,但是有些情況下是會造成莫名的錯誤。

              我們先來搞清楚在JVM創建新的對象時,主要要經過三步。

              1.分配記憶體

              2.初始化構造器

              3.將對象指向分配的記憶體的地址

              這種順序在上述雙重加鎖的方式是沒有問題的,因為這種情況下JVM是完成了整個對象的構造才將記憶體的地址交給了對象。但是如果2和3步驟是相反的(2和3可能是相反的是因為JVM會針對位元組碼進行調優,而其中的一項調優便是調整指令的執行順序),就會出現問題了。

              我們假設2與3位置相反了,針對上述的雙重加鎖來講,因為這時會先將記憶體地址賦給對象myClass,然後再進行初始化構造器,這時候後面的線程去請求getInstance方法時,會認為myClass對象已經實例化了,直接返回一個引用。如果在初始化構造器之前,這個線程使用了myClass,就會產生莫名的錯誤。

那麼我們要如何避免這一個問題呢?我們可以給靜態的實例屬性加上關鍵字volatile,這樣就不會出現實例化發生一半的情況,因為加入了volatile關鍵字,就等於禁止了JVM自動的指令重排序優化,並且強行保證線程中對變數所做的任何寫入操作對其他線程都是即時可見的。volatile會強行將對該變數的所有讀和取操作綁定成一個不可拆分的動作。由於本節我們講的是設計模式,所以這裡LZ不會去詳細介紹volatile以及JVM中變數訪問時所做的具體動作(或者以後LZ會單獨將),感興趣的讀者可以去翻閱相關的資料。

             另外由於volatile關鍵字是在JDK1.5版本出現的,所以凡是1.4及1.4之前的版本都無法使用。這裡LZ把這種寫法完整的列出來。

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

另外,這就是我們常說的“懶漢式”,大家可以這樣記“因為懶漢太懶了,所以只有用的時候才創建對象。”

  懶漢式單例類。   只在外部對象第一次請求實例的時候才會去創建
    優點:第一次調用時才會初始化,避免記憶體浪費。
    缺點:必須加鎖synchronized 才能保證單例,效率低

當然,除了這種寫法,我們還有一種辦法可以解決線程併發的問題,相信大家都聽過“餓漢式”
 class MyClassTo {
    
    private static MyClassTo myClassTo = new MyClassTo();
    
    private MyClassTo(){}
    
    public static MyClassTo getInstance(){
        return myClassTo;
    }
    
}

因為太餓了,所以上來就創建=。=

       餓漢式單例類。    它在類載入時就立即創建對象。
       優點:沒有加鎖,執行效率高。  用戶體驗上來說,比懶漢式要好。
       缺點:類載入時就初始化,浪費記憶體

那麼為什麼餓漢比懶漢要好,一個是空間換時間,一個是時間換空間,你們說是時間終於還是空間重要?=。=
另外,還有一種單例模式,被稱為"登記式"
  class MyClassThree{
        private MyClassThree(){}
        public static MyClassThree getInstance(){ return SINGLETON.myClassThree;}
        private static class SINGLETON{//內部類
            private static final MyClassThree myClassThree= new MyClassThree();
        }
    }

內部類只有在外部類被調用才載入,產生SINGLETON實例,又不用加鎖,這個模式有上述倆模式的優點,屏蔽了他們的缺點,是最好的單例模式。

首先來說一下,這種方式為何會避免了雙重加鎖的漏洞,主要是因為一個類的靜態屬性只會在第一次載入類時初始化,這是JVM幫我們保證的,所以我們根本無需擔心併發訪問的問題。所以在初始化進行一半的時候,別的線程是無法使用的,因為JVM會幫我們強行同步這個過程。另外由於靜態變數只初始化一次,所以singleton仍然是單例的。

那麼我們總結一下這種模式幫助我們做到了什麼:

             1.在不考慮反射強行突破訪問限制的情況下,MyClassThree最多只有一個實例 

             2.保證了併發訪問的情況下,不會由於初始化動作未完全完成而造成使用了尚未正確初始化的實例。

     3.保證了併發訪問的情況下,不會發生由於併發而產生多個實例。

 好了,到這裡單例模式LZ就講完了,下期預告,等下次再說=。=



 

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 先導入模塊: from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 分頁器的使用(views函數中的代碼): templates中的代碼 ...
  • 熔斷是當某個服務調用慢或者有大量超時現象(過載),系統停止後續針對該服務的調用而直接返回,直至情況好轉才恢復調用。這通常是為防止造成整個系統故障而採取的一種保護措施,也稱過載保護。很多時候剛開始,可能只是出現了局部小規模系統故障,但後來故障影響的範圍越來越大,最終導致了全局性的後果。 ...
  • 這是曾經的一個面試題,正好引出狀態機編程思想。挺不錯的一個例子。 題目描述 給定一個字元串,它由以下字元組成: 左括弧“(” 右括弧“)” 下劃線“_” 大小寫字母構成的字元串(單字母也算作字元串) 該字元串組成有以下規則限定: 括弧成對出現且不會嵌套,保證語法正確 字元串可以出現在括弧內,也可以出 ...
  • 閑暇之餘,開發了一款休閑類app,雖然用戶量不多,但確實花了不少心血在這上面。然而,開發出來的結果,與之前想好的架構,還是有不少區別。 下麵,記錄下這款app架構的演變: 最初,只想寫個app,能與機器人進行聊天。架構隨意搭(或者說沒有架構),快速開發出來就好: 最初(無架構)版本 很簡單,按照代碼 ...
  • 對於大型網站,分層和分隔的一個主要目的是為了切分後的模塊便於分散式部署,即將不同模塊部署在不同的伺服器上,通過遠程調用協同工作。分散式意味著可以使用更多的電腦完同樣的工作,電腦越多,CPU、記憶體、存儲資源就越多,能過處理的併發訪問和數據量就越大,進而能夠為更多的用戶提供服務。 ...
  • Pipeline使用了groovy語法,同時可以使用所有jenkins插件在groovy里進行調用,可以說通過UI可以實現的功能使用pipeline也可以實現,這一點我在上一篇文章里已經說明,今天主要說一下pipeline里的公用類庫,你可以自己定義方法,一般地一個方法一個文件,擴展名為groovy ...
  • 各位朋友好,本章節我們繼續講第五個設計模式。 在生活中,我們都知道手機記憶體卡是無法直接接電腦的,因為記憶體卡的卡槽比較小,而電腦只有USB插孔,此時我們需要用到讀卡器。這個讀卡器就相當於是適配器。這是生活上的適配器,那麼在OO對象中,適配器就是將一個介面轉換成另一個介面,使得客戶可以使用。 適配器模式 ...
  • 一、背景&問題 之前框架是一個基於SOA思想設計的分散式框架。各應用通過服務方式提供使用,服務之間通信是RPC方式調用,具體實現基於.NET的WCF通信平臺。框架存在如下2個問題: 1、高併發處理能力不足。一當高併發請求,可能出現多個服務待定處理,導致整個系統出現瓶頸。 2、隨著移動端廣泛應用,服務 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...