­

設計模式|06 單例模式

  • 2019 年 10 月 10 日
  • 筆記

什麼是單例模式

權威定義:確保一個類只有一個實例,並提供一個全局訪問點。 博主的理解:雖說單例模式對於許多人來說並不難,但是其中也是有很多需要注意的細節的。

設計方案

方案一:「急切」的單例

思路:所謂急切,是指我們在一開始的時候就創建出類的單例實例,不管有無實際需求。滿足了單例設計模式的需求。

package singleton;    /**   * 急切式單例   */  public class SingletonEagerly {    //    靜態變量設置全局的單例singletonEagerly      public static SingletonEagerly singletonEagerly = new SingletonEagerly();      //    私有化構造器      private SingletonEagerly() {      }  }

設計關鍵:私有化構造器,靜態變量new一個類實現單例; 設計問題:如果一直不需要這個單例,或者系統設計中有一大堆的單例,那麼會影響性能!

方案二 「懶漢式」單例

思路:類提供一個供對外訪問的靜態方法getInstance()來對外提供一個全局訪問點,然後通過私有化構造器,實現單例模式。

package singleton;    /**   * 懶漢式單例模式   */  public class SingletonLazy {  //    設置一個私有的靜態變量定義單例      private static SingletonLazy singletonLazy;  //    私有化構造器      private SingletonLazy(){};        /**       * 對外提供一個獲取單例的全局訪問點 需要公開       * @return       */      public static SingletonLazy getInstance(){  //        進行判斷          if(singletonLazy==null){              return new SingletonLazy();          }          return singletonLazy;      }    }

設計關鍵:私有化構造器、對外提供getInstance獲取單例 設計問題:無法應對多線程同時訪問的情況,如果出現線程A進入到getInstance的判斷為null之後被線程B搶佔了資源,則B也會重新建立一個實例,而線程A也會建立一個實例,這就會出現兩個單例的情況,不滿足要求。

方案三 多線程模式

思路:通過synchronized關鍵字讓getInstance方法變為同部方法就能輕鬆解決問題了

package singleton;    /**   * 懶漢式單例模式   */  public class SingletonThread {  //    設置一個私有的靜態變量定義單例      private static SingletonThread SingletonThread;  //    私有化構造器      private SingletonThread(){};        /**       * 對外提供一個獲取單例的全局訪問點 需要公開       * synchronized保證為同步方法       * @return       */      public static synchronized SingletonThread getInstance(){  //        進行判斷          if(SingletonThread==null){              return new SingletonThread();          }          return SingletonThread;      }    }

設計關鍵:使用synchronized關鍵字實現線程同步 設計問題:每當需要獲取實例都要通過這個同步方法,效率會下降至少一百倍;實際上只有第一次是需要進行同步操作的,創建實例之後的同步就成了累贅了!

方案四 雙重檢查加鎖的單例模式

思路:雙重檢查機制讓方法只會第一次被鎖上,以後的情況則不會出現需要同步的情況。

package singleton;    /**   * 懶漢式單例模式   */  public class Singleton {      //    設置一個私有的靜態變量定義單例  //    這裡注意我們給實例加了一個volatile關鍵字  //    實現了多線程環境下,變量只有一個版本!保證了可見性!      private static volatile Singleton singleton;        //    私有化構造器      private Singleton() {      }        ;        /**       * 對外提供一個獲取單例的全局訪問點 需要公開       * synchronized保證同步整個類       *       * @return       */      public static Singleton getInstance() {  //        第一重判斷          if (singleton == null) {  //            第一次初始化才會進入這裡同步方法              synchronized (Singleton.class) {  //                第二重判斷                  if (singleton == null) {                      return new Singleton();                  }              }          }          return singleton;      }  }  

設計關鍵:給實例加了一個volatile關鍵字,實現了多線程環境下,變量只有一個版本! 保證了可見性!同時我們給getInstance方法提供了雙重檢查機制,讓方法不會一直被同步。 設計問題:會有一些複雜

方案五 私有化靜態內部類

package singleton;    /**   * 靜態私有內部類、支持多並發、效率高、   */  public class Singleton1 {      private Singleton1() {}        /**       * 靜態化一個內部類 用於創建單例       */      private static class SingletonHolder {          private static Singleton1 instance = new Singleton1();      }        public static Singleton1 getInstance() {          return SingletonHolder.instance;      }  }  

設計關鍵:通過靜態私有化內部類,高效、且在需要的時候才會被創建 高效支持高並發

回到定義

容易發現其實上面的四個單例模式的方案都是可以滿足單例設計模式的要求的,至於我們又該如何做出選擇,就要看具體的需求了,在確定性能以及資源的限制下,我們可以選擇適合自己的單例模式,譬如如果沒有什麼限制,使用方案一急切式的單例模式也是可以的,但是這個直接使用全局變量的問題是,我們可能會造成在系統日漸複雜時候,用這麼多的全局變量指向許多小對象會使得命名空間被「污染」;同時這個單例的控制權不在我么自己的手中,而是掌握在了JVM的手中。

Java中使用到的單例模式

spirng框架

對於最常用的spring框架來說,我們經常用spring來幫我們管理一些無狀態的bean,其默認設置為單例,這樣在整個spring框架的運行過程中,即使被多個線程訪問和調用,這些「無狀態」的bean就只會存在一個,為他們服務。那麼「無狀態」bean指的是什麼呢?

  1. 無狀態:當前我們託管給spring框架管理的javabean主要有service、mybatis的mapper、一些utils,這些bean中一般都是與當前線程會話狀態無關的,沒有自己的屬性,只是在方法中會處理相應的邏輯,每個線程調用的都是自己的方法,在自己的方法棧中。
  2. 有狀態:指的是每個用戶有自己特有的一個實例,在用戶的生存期內,bean保持了用戶的信息,即「有狀態」;一旦用戶滅亡(調用結束或實例結束),bean的生命期也告結束。即每個用戶最初都會得到一個初始的bean,因此在將一些bean如User這些託管給spring管理時,需要設置為prototype多例,因為比如user,每個線程會話進來時操作的user對象都不同,因此需要設置為多例。

優勢:

  1. 減少了新生成實例的消耗,spring會通過反射或者cglib來生成bean實例這都是耗性能的操作,其次給對象分配內存也會涉及複雜算法;
  2. 減少jvm垃圾回收;
  3. 可以快速獲取到bean;

劣勢: 單例的bean一個最大的劣勢就是要時刻注意線程安全的問題,因為一旦有線程間共享數據變很可能引發問題。

log4j

在使用log4j框架時也注意到了其使用的是單例,當然也為了保證單個線程對日誌文件的讀寫時不出問題