1. 程式人生 > 實用技巧 >設計模式 --單例模式

設計模式 --單例模式

前言

單例模式應該是我們最熟悉的模式了,如果說要隨便抓一個程式設計師,讓他說一說最熟悉的集中設計模式,我想肯定有單例模式。

我們這節就全面的來講解一下單例模式。

為什麼要用單例模式

單例模式理解起來非常簡單。在一個系統中,一個類只允許建立一個物件,那這個類就是單例類,這種設計模式就叫做單例設計模式。

為什麼需要單例模式呢?首先我們得熟知他的運用場景。就是某個類比如工廠類,配置類,我們系統中只需要一份得,無需多次重複建立的類,我們就可以用單例模式。

單例模式的實現方式

餓漢式

餓漢式的實現方式比較簡單。在類載入的時候,instance靜態例項就已經建立並初始化好了,所以,instance例項的建立過程是執行緒安全的。不過,這樣的實現方式不支援延遲載入(在真正使用到的時候,再建立),程式碼如下:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

IdGenerator類是被靜態變數修飾並且是直接例項化的。所以當我們呼叫getInstance()方法,jvm會載入IdGenerator,(載入的過程中,jvm會經歷載入->驗證->準備->解析->初始化,該過程是天然加鎖的),初始化完成以後成員變數instance就指向了IdGenerator例項物件。又因為IdGenerator的構造方法是private修飾的。所以通過該方法我們就實現了餓漢式的單例模式。

懶漢式

有餓漢式,就有對應的懶漢式。懶漢式相對於餓漢式的優勢是支援延遲載入。具體程式碼如下:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {}
  public static synchronized IdGenerator getInstance() {
    if (instance == null) {
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

不過懶漢式的缺點也很明顯,我們給getInstance()方法加了一把大鎖,導致這個函式的併發度很低。量化一下的話,併發度是1,也就相當於序列操作了。而這個函式實在單例使用期間,一直會被呼叫。如果這個單例類偶爾會被用到,那這種實現方式還可以接受。但是,如果頻繁地使用到,那頻繁的加鎖和釋放鎖以及併發度低等問題,會導致效能很差。

雙重檢查鎖

餓漢式不支援延遲載入,懶漢式有效能問題。那麼有沒有一種及支援延遲載入又支援高併發的單例實現方式呢?

有的,雙重檢查鎖登場~我們來看如下程式碼:

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static volatile IdGenerator instance;
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    if (instance == null) {
      synchronized(IdGenerator.class) { // 此處為類級別的鎖
        if (instance == null) {
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

這種實現方式中,只要instance被建立之後,即使再呼叫geiInstance()函式也不會再進入加鎖邏輯中。所以,這種方式實現解決了懶漢式併發度低的問題。

注意,我們給instance成員變數加上了volatile的關鍵字。為什麼呢?

重排序可能會讓該程式碼出現問題,因為我們new一個物件的順序是 開闢記憶體空間->建立物件->指向該記憶體空間。如果我們不加上volatile關鍵字,就可能會發生重排序,變成 開闢記憶體空間->指向該記憶體空間->建立物件。

大家想一想,是不是就有可能導致IdGenerator物件被new出來,並且賦值給instance之後,並沒有來得及初始化,就被另一個執行緒使用了。

靜態內部類

我們再來看一種比雙重檢查鎖更加簡單的實現方法,那就是利用java的靜態內部類。它有點類似餓漢式,但又能做到延遲載入。我們來看看它的實現:


public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {}

  private static class SingletonHolder{
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    return SingletonHolder.instance;
  }
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

SingletonHolder是一個靜態內部類,當外部類IdGenerator被載入的時候,並不會建立SingletonHolder例項物件。只有當呼叫getInstance()方法的時候,SingletonHolder才會被載入,這個時候才會建立instance。instance的唯一性、建立過程的執行緒安全性,都由JVM來保證,所以,這種實現方式既保證了執行緒安全,又能做到延遲載入。

列舉

我們再來看最後一種實現方式,列舉。

這種方式通過java列舉型別本身的特性,保證了例項建立的執行緒安全性和例項的唯一性。具體程式碼如下:

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

總結

單例模式是設計模式中比較簡單一種,也是每個程式設計師必須掌握的設計模式。五種實現方式都應該掌握。

我們最後再討論一個問題,延遲載入的好壞?

其實就我而言,我反而覺得餓漢式是最簡單,也是相對最優的實現方式。為什麼呢?其實大家好像有一個共識,提前初始化是一種浪費資源的行為。最好的方式應該是再用到的時候再去初始化。但是仔細想一想真的是這樣的嗎?

我們大部分同學作為業務性開發。而單例模式是建立好就一直存在我們系統中。如果說建立的過程中發生資源不夠,或者異常的時候。我們是希望在系統啟動的時候就發現還是在系統執行到一半的時候呢?

肯定是系統一建立我們就發現問題,就能立即去修復。這樣也能避免程式在執行一段時間後,突然因為初始化這個例項佔用資源過多,導致系統奔潰,影響系統的可用性。所以我的建議還是作為業務性開發,儘量還是使用餓漢式~

如果你有什麼不同的想法,歡迎留言~