1. 程式人生 > >單例模式新談(包含三種方式)

單例模式新談(包含三種方式)

  設計模式是一套被反覆使用,多數人知曉,經過分類編目的,程式碼設計的總結,也可以說是前人的智慧結晶。學習設計模式能讓我們對一些應用場景使用相同的套路達到很好的效果,我會不定時更新一些自己對設計模式的理解的文章,從定義,實現,應用場景來說說設計模式,今天我要說的物件是單例模式
一,定義
  什麼是單例模式,字面理解,這種設計模式的目的是在一定程度上保證物件在程式中只有一個,或者只被建立一次,之所以說在一定程度上是因為還有還有反射機制存在,如果不考慮反射的話,確實是可以保證物件唯一性的,這裡我們不考慮反射。
二,實現
  那麼如何保證物件的唯一性呢,可以從兩方面出發,第一,物件的建立,第二,物件的獲取。如果我們只給目標類提供一個構造方法,並且把這個構造方法私有化(private),那麼就可以保證物件無法被建立了(注意,一定要私有化,有的同學會想,我不寫構造方法是不是就可以了,注意不寫是會報錯的,因為所有的類都必須從Object這個父類中繼承無參構造方法,有的同學又會說,我沒寫也沒報錯啊,那是因為你的開發工具,或者工具框架預設給你建立了!),當然物件絕對的無法建立是沒有意義的,物件都沒發建立了,那這個物件存在也不能發揮作用,所以我們對外提供一個唯一的獲取物件的方法,由於不能建立物件,所以這個方法必須是靜態的(static),然後保證獲取的這個物件是唯一的,就可以達到我們的目的。那麼,如果保證我們提供的物件是唯一的呢,從實現方式來說,可以把設計模式分為3類,分別是餓漢式,懶漢式,登記式
  1.餓漢式


    把物件理解為麵包,一個餓漢對面包的態度是怎樣的,肯定是希望自己馬上就擁有面包,餓漢式的原理也是如此,在程式初始化的時候就建立物件。下面展示餓漢式的建立程式碼
    public class SingleCase{

      private static SingleCase singleCase=initCase();

      private SingleCase(){}//私有化構造方法

      private static SingleCase initCase(){
        //這裡就可以寫具體的物件初始化資訊了
        //由於這裡是演示程式碼,我就簡單的new一下
        return new SingleCase();
      }
      public static SingleCase getInstance(){

        return singleCase;

      }
    }
  2.懶漢式
    同樣把物件理解為麵包,一個懶漢的態度就不一樣了,因為很懶,在他不餓,或者說不需要的時候,他是不會去拿面吧的(建立物件),所以懶漢式的核心思想是:需要的時候再建立物件。下面演示懶漢式單例建立程式碼,為了讓看客們更容易看懂,我們在餓漢式的基礎上做修改
    public class SingleCase{

      private static SingleCase singleCase=null;//這裡沒有呼叫建立物件的方法,所以初始化的時候的singleCase值是空的

      private SingleCase(){}

      private static SingleCase initCase(){

      return new SingleCase();

      }
      public static SingleCase getInstance(){

        //相比於餓漢式,這裡多了一個判斷,如果singleCase是空的,就建立,如果singleCase不是空的,那就直接返回
          if (singleCase==null){

            return initCase();

          } 
        return singleCase;
      }
    }
    以上程式碼就是懶漢式單例模式的實現方式,但是細心的看客可能發現了一個問題,就getInstance()方法如果被多執行緒訪問的話,可能導致建立多個物件。我們知道,java是一門多執行緒語言,cpu的執行也並不是連續的,假設一臺計算機同時有10個程序在執行,每個程序3個執行緒,那總共就有30個執行緒,cpu的工作就是在這30個執行緒之間快速的切換,執行一下1號執行緒,再執行一下2號執行緒,再執行一下8號執行緒,再執行一下18號執行緒,再執行一下一號執行緒,這種切換機制是隨機的,在一秒鐘之內,可能有的執行緒被執行100次,也有的執行緒可能被執行10次。那麼再上面的判斷中,可能會出現某個執行緒剛執行判斷if (singleCase==null)還沒來得及執行initCase()方法cpu就切換到另外一個執行緒,而這個執行緒也在執行if (singleCase==null)判斷,而這時候物件還未被建立,所以也會進入if後面的程式碼塊,最終導致initCase()被執行兩次,也就是建立了2個物件,這顯然和我們的初衷不和。為了保證物件的性,讓方法只被執行一次,我們這裡使用synchronized關鍵字,synchronized作用在靜態方法上,可以保證在整個程式中方法只能同時被一個執行緒執行,這個執行緒在執行的時候會擁有這個方法的鎖,導致其他執行緒無法獲取鎖,也就無法執行,當該執行緒執行完了之後,會釋放這個鎖,然後才能被其他執行緒執行(關於synchronized關鍵字,如果有看客不清楚用法可以在評論區留言,如果人數多的話我再開個單章講講這個關鍵字),下面展示加synchronized之後的程式碼,只需要在原有的方法上直接加上去即可
    public class SingleCase{

      private static SingleCase singleCase=null;

      private SingleCase(){}

      private static SingleCase initCase(){

        return new SingleCase();

      }
      //直接載入方法上
      public synchronized static SingleCase getInstance(){

        if (singleCase==null)

            return initCase();

        return singleCase;
        }
    }
  以上寫法確實可以解決高併發的時候創建出多個物件的現象,但是並不完美,因為加了synchronized之後,每次呼叫方法都會加鎖,造成阻塞,影響效能,而我們其實僅僅只需要保證在singleCase==null建立物件的時候上鎖就可以了,而大部分情況是物件已經建立好了,直接獲取的,我們不希望影響這種情況的效能。那麼,我們可以把鎖放在確定物件沒有被建立之後,程式碼更改如下
    class SingleCase{

      private static SingleCase singleCase=null;

      private SingleCase(){}

      private static SingleCase initCase(){

        return new SingleCase();

      }
      public static SingleCase getInstance(){

        if (singleCase==null){

          synchronized (SingleCase.class){//synchronized的用法還請各看客自行學習,或者評論區留言

            if (singleCase==null){

              singleCase=initCase();

            }
          }
        }
        return singleCase;
      }
    }
    關於synchronized (SingleCase.class){}裡面的程式碼塊又加了一個if (singleCase==null)的判斷,可能有看客會奇怪,為什麼這裡還要加一次判斷,其實這裡跟上面說的一樣,第一次singleCase==null的判斷可能出現在多個執行緒中,導致多個執行緒進入判斷後面的程式碼塊,雖然不能進入被synchronized鎖定的程式碼塊,但是隻要有執行緒進入了第一次singleCase==null判斷之後的程式碼塊,當上一個擁有synchronized鎖的執行緒執行完建立物件的程式碼釋放鎖之後,當前執行緒會繼續執行synchronized鎖後面的程式碼再次建立物件,而這裡再加一個判斷,就可以解決這個問題
  3.登記式
    還是把物件理解為麵包,不同於懶漢式和餓漢式的單個麵包,登記式也可以說是廚師式,因為它操作的是多種麵包,就像廚師一樣,他擁有很多面包。那麼多個麵包怎麼存放呢?麵包櫃Map!就像我們在店裡看到的麵包櫃一樣,每種麵包都有對應的名字,就是我們這裡的key,一個名字對應一個麵包,一個key對應一個物件。為了讓這個key被所有人熟知,登記式的可以一般使用class.getName();我們知道單例模式的第一設計原則就是私有化構造方法,想要在一個類中儲存多個物件,又不能通過new的方法,那只有一條路可以走了,反射。登記式就是利用反射來建立物件,到這裡肯定有看客會說,既然用到反射了,那麼不通過你的麵包櫃,我也可以拿到麵包吧,的確如此,這也是我個人比較詬病登記式的一個地方。不過登記式在特殊的場景中能發揮極其強大的作用,比如Spring的bean容器!Spring的bean容器不完全是登記式,不過實現方式上有很多相通的地方。登記式相比於懶漢式有更豐富的建立方法,也涉及執行緒安全問題,這裡不一一演示,請各位看客自行實驗,下面展示登記式單例的簡單建立方法
      public class SingleCase2 {

        private static Map singletonMap = new HashMap();

        private SingleCase2() {}

        public static Object getInstance(String className) throws Exception{

          if (!singletonMap.containsKey(className)) {

            singletonMap.put(className, Class.forName(className).newInstance());

          }
          return singletonMap.get(className);
        }
      }
三.應用場景
  單例模式的應用場景在網上隨便搜尋一下能出來幾百篇文章,大多長篇大論晦澀難懂,我喜歡用一些簡單的話說明一些簡單的道理。我們在說什麼時候用單例時不如先來想想單例能實現的效果,首先單例能現實的效果是能保證程式中目標類的物件只有一個,那麼什麼時候需要保證類的物件只有一個呢,比如,某個配置檔案類的的物件,我們肯定希望每次訪問的時候配置資訊是一致的,如果檔案資訊有更新的話,直接就能從對應的類的物件中讀取到,如果這個類的物件有多個,可能會造成資訊不一致,或者更新不及時。另外,儲存一些公共資源時,比如java的執行緒池,我們希望對某個執行緒的數量有一個準確的限制,如果不適用單例,控制起來是不是就會非常麻煩。然後,單例的另一個優點,減少資源消耗,當物件需要被大量訪問的時候,是每次建立一個物件呢,還是重複適用同一個物件,當然是使用同一個物件佔用的資源少,Spring就是這麼幹的。那麼又什麼時候使用懶漢式,什麼時候使用餓漢式呢,如果你的物件再 程式啟動之後馬上就要使用的,需要初始化時就建立物件,那麼毫無疑問選擇餓漢式,如果你的物件指不定什麼時候用,又不或者可能根本用不到,那麼使用懶漢式可能比較經濟一點。至於登記模式,但從使用的時間來說,可以根據需要設計成懶漢式或者餓漢式,例子中是懶漢式。當然,登記模式一般在複雜場景中發揮重要的作用,比如Spring,而我們實際開發中運用較少。
  ps:今天就說這麼多,如果你喜歡,請幫忙點贊,評論,轉發。你的的肯定是我寫下去的動力