深刻理解單例模式

初遇設計模式在上個寒假,當時把每一個設計模式過了一遍,對設計模式有了一個最初級的瞭解。這個學期借了幾本設計模式的書籍看,聽了老師的設計模式課,對設計模式算是有個更進一步的認識。後面可能會不按期更新一下本身對於設計模式的理解。每一個設計模式看似很簡單,實則想要在一個完整的系統中應用仍是很是很是難的。而後個人水品也很是很是有限,代碼量也不是不少,只能經過閱讀書籍、思考別人的編碼經驗以及結合本身的編碼過程當中遇到的問題來總結。java

怎麼用->怎麼用纔好->怎麼與其餘模式結合使用,我想這是每一個開發人員都須要逾越的一道鴻溝。git

本文主要內容

本文主要內容

1 單例模式簡介

1.1 定義

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。github

1.2 爲何要用單例模式呢?

在咱們的系統中,有一些對象其實咱們只須要一個,好比說:線程池、緩存、對話框、註冊表、日誌對象、充當打印機、顯卡等設備驅動程序的對象。事實上,這一類對象只能有一個實例,若是製造出多個實例就可能會致使一些問題的產生,好比:程序的行爲異常、資源使用過量、或者不一致性的結果。面試

簡單來講使用單例模式能夠帶來下面幾個好處:

  • 對於頻繁使用的對象,能夠省略建立對象所花費的時間,這對於那些重量級對象而言,是很是可觀的一筆系統開銷;
  • 因爲 new 操做的次數減小,於是對系統內存的使用頻率也會下降,這將減輕 GC 壓力,縮短 GC 停頓時間。設計模式

    1.3 爲何不使用全局變量確保一個類只有一個實例呢?

咱們知道全局變量分爲靜態變量和實例變量,靜態變量也能夠保證該類的實例只存在一個。
只要程序加載了類的字節碼,不用建立任何實例對象,靜態變量就會被分配空間,靜態變量就能夠被使用了。緩存

可是,若是說這個對象很是消耗資源,並且程序某次的執行中一直沒用,這樣就形成了資源的浪費。利用單例模式的話,咱們就能夠實如今須要使用時才建立對象,這樣就避免了沒必要要的資源浪費。 不只僅是由於這個緣由,在程序中咱們要儘可能避免全局變量的使用,大量使用全局變量給程序的調試、維護等帶來困難。安全

2 單例的模式的實現

一般單例模式在Java語言中,有兩種構建方式:

  • 餓漢方式。指全局的單例實例在類裝載時構建
  • 懶漢方式。指全局的單例實例在第一次被使用時構建。

不論是那種建立方式,它們一般都存在下面幾點類似處:微信

  • 單例類必需要有一個 private 訪問級別的構造函數,只有這樣,才能確保單例不會在系統中的其餘代碼內被實例化;
  • instance 成員變量和 uniqueInstance 方法必須是 static 的。

2.1 餓漢方式(線程安全)

public class Singleton {
       //在靜態初始化器中建立單例實例,這段代碼保證了線程安全
        private static Singleton uniqueInstance = new Singleton();
        private Singleton(){}
        public static Singleton getInstance(){
            return uniqueInstance;
        }
    }

所謂 「餓漢方式」 就是說JVM在加載這個類時就立刻建立此惟一的單例實例,無論你用不用,先建立了再說,若是一直沒有被使用,便浪費了空間,典型的空間換時間,每次調用的時候,就不須要再判斷,節省了運行時間。多線程

## 2.2 懶漢式(非線程安全和synchronized關鍵字線程安全版本 )架構

public class Singleton {  
      private static Singleton uniqueInstance;  
      private Singleton (){
      }   
      //沒有加入synchronized關鍵字的版本是線程不安全的
      public static Singleton getInstance() {
          //判斷當前單例是否已經存在,若存在則返回,不存在則再創建單例
          if (uniqueInstance == null) {  
              uniqueInstance = new Singleton();  
          }  
          return uniqueInstance;  
      }  
 }

所謂 「餓漢方式」 就是說單例實例在第一次被使用時構建,而不是在JVM在加載這個類時就立刻建立此惟一的單例實例。

可是上面這種方式很明顯是線程不安全的,若是多個線程同時訪問getInstance()方法時就會出現問題。若是想要保證線程安全,一種比較常見的方式就是在getInstance() 方法前加上synchronized關鍵字,以下:

public static synchronized Singleton getInstance() {  
          if (instance == null) {  
              uniqueInstance = new Singleton();  
          }  
          return uniqueInstance;  
      }

咱們知道synchronized關鍵字偏重量級鎖。雖然在JavaSE1.6以後synchronized關鍵字進行了主要包括:爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各類優化以後執行效率有了顯著提高。

可是在程序中每次使用getInstance() 都要通過synchronized加鎖這一層,這不免會增長getInstance()的方法的時間消費,並且還可能會發生阻塞。咱們下面介紹到的 雙重檢查加鎖版本 就是爲了解決這個問題而存在的。

2.3 懶漢式(雙重檢查加鎖版本)

利用雙重檢查加鎖(double-checked locking),首先檢查是否實例已經建立,若是還沒有建立,「才」進行同步。這樣以來,只有一次同步,這正是咱們想要的效果。

public class Singleton {

    //volatile保證,當uniqueInstance變量被初始化成Singleton實例時,多個線程能夠正確處理uniqueInstance變量
    private volatile static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getInstance() {
       //檢查實例,若是不存在,就進入同步代碼塊
        if (uniqueInstance == null) {
            //只有第一次才完全執行這裏的代碼
            synchronized(Singleton.class) {
               //進入同步代碼塊後,再檢查一次,若是還是null,才建立實例
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

很明顯,這種方式相比於使用synchronized關鍵字的方法,能夠大大減小getInstance() 的時間消費。

咱們上面使用到了volatile關鍵字來保證數據的可見性,關於volatile關鍵字的內容能夠看個人這篇文章:
《Java多線程學習(三)volatile關鍵字》: https://blog.csdn.net/qq_34337272/article/details/79680771

注意: 雙重檢查加鎖版本不適用於1.4及更早版本的Java。
1.4及更早版本的Java中,許多JVM對於volatile關鍵字的實現會致使雙重檢查加鎖的失效。

2.4 其餘方式(枚舉)

除了上面說的幾種建立方式以外,還有挺多種其餘的建立方式這裏稍微多提一點使用枚舉的方式,其餘建立方式咱們就無論了,沒有什麼實質性的做用。

枚舉實現單例的優勢就是簡單,可是大部分應用開發不多用枚舉,可讀性並非很高。我的感受懶漢式(雙重檢查加鎖版本)仍是使用挺多的,這種方式的可讀性也比較好。

public enum Singleton {
     //定義一個枚舉的元素,它就是 Singleton 的一個實例
    INSTANCE;  
    
    public void doSomeThing() {  
         System.out.println("枚舉方法實現單例");
    }  
}

使用方法:

public class ESTest {

    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomeThing();//output:枚舉方法實現單例

    }

}

《Effective Java 中文版 第二版》

這種方法在功能上與公有域方法相近,可是它更加簡潔,無償提供了序列化機制,絕對防止屢次實例化,即便是在面對複雜序列化或者反射攻擊的時候。雖然這種方法尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。 —-《Effective Java 中文版 第二版》

《Java與模式》

《Java與模式》中,做者這樣寫道,使用枚舉來實現單實例控制會更加簡潔,並且無償地提供了序列化機制,並由JVM從根本上提供保障,絕對防止屢次實例化,是更簡潔、高效、安全的實現單例的方式。

2.5 總結

咱們主要介紹到了如下幾種方式實現單例模式:

  • 餓漢方式(線程安全)
  • 懶漢式(非線程安全和synchronized關鍵字線程安全版本)
  • 懶漢式(雙重檢查加鎖版本)
  • 枚舉方式

參考:

《Head First 設計模式》

《Effective Java 中文版 第二版》

【Java】設計模式:深刻理解單例模式

我是Snailclimb,一個以架構師爲5年以內目標的小小白。
歡迎關注個人微信公衆號:"Java面試通關手冊"(一個有溫度的微信公衆號,期待與你共同進步~~~堅持原創,分享美文,分享各類Java學習資源):

個人公衆號

相關文章
相關標籤/搜索