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

来源: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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...