天天看點

單例模式 - 隻有一個執行個體

程式在運作的時候,通常會有很多的執行個體。例如,我們建立 100 個字元串的時候,會生成 100 個 string 類的執行個體。

但是,有的時候,我們隻想要類的執行個體隻存在一個。例如,「你猜我畫」中的畫闆,在一個房間中的使用者需要共用一個畫闆執行個體,而不是每個使用者都配置設定一個畫闆的執行個體。

此外,對于資料庫連接配接、線程池、配置檔案解析加載等一些非常耗時,占用系統資源的操作,并且還存在頻繁建立和銷毀對象,如果每次都建立一個執行個體,這個系統開銷是非常恐怖的,是以,我們可以始終使用一個公共的執行個體,以節約系統開銷。

像這樣確定隻生成一個執行個體的模式,我們稱之為 單例模式。

單例模式的目的在于,一個類隻有一個執行個體存在,即保證一個類在記憶體中的對象唯一性。

現在,我們來了解這個類圖。

單例模式 - 隻有一個執行個體

singleton 類定義的靜态的 instance 成員變量,并将其初始化為 singleton 類的執行個體。這樣,就可以保證單例類隻有一個執行個體。

singleton 類的構造方法是私有的,這個設計的目的在于,防止類外部調用該構造方法。單例模式必須要確定在任何情況下,都隻能生成一個執行個體。為了達到這個目的,必須設定構造方法為私有的。換句話說,singleton 類必須自己建立自己的唯一執行個體。

構造方法是私有的,那麼,我們需要提供一個通路 singleton 類執行個體的全局通路方法。

保證一個類隻有一個執行個體,并提供一個通路它的全局通路方法。

顧名思義,類一加載對象就建立單例對象。

值得注意的是,在定義靜态變量的時候執行個體化 singleton 類,是以在類加載的時候就可以建立了單例對象。

此時,我們調用兩次 singleton 類的 getinstance() 方法來擷取 singleton 的執行個體。我們發現 s1 和 s2 是同一個對象。

懶漢式,即延遲加載。單例在第一次調用 getinstance() 方法時才執行個體化,在類加載時并不自動執行個體化,在需要的時候再進行加載執行個體。

在多線程中,如果使用懶漢式的方式建立單例對象,那就可能會出現建立多個執行個體的情況。

為了避免多個線程同時調用 getinstance() 方法,我們可以使用關鍵字 synchronized 進行線程鎖,以處理多個線程同時通路的問題。每個類執行個體對應一個線程鎖, synchronized 修飾的方法必須獲得調用該方法的類執行個體的鎖方能執行, 否則所屬線程阻塞。方法一旦執行, 就獨占該鎖,直到從該方法傳回時才将鎖釋放。此後被阻塞的線程方能獲得該鎖, 重新進入可執行狀态。

上面的案例,在多線程中很好的工作而且是線程安全的,但是每次調用 getinstance() 方法都需要進行線程鎖定判斷,在多線程高并發通路環境中,将會導緻系統性能下降。事實上,不僅效率很低,99%情況下不需要線程鎖定判斷。

這個時候,我們可以通過雙重校驗鎖的方式進行處理。換句話說,利用雙重校驗鎖,第一次檢查是否執行個體已經建立,如果還沒建立,再進行同步的方式建立單例對象。

單例模式 - 隻有一個執行個體

枚舉的特點是,構造方法是 private 修飾的,并且成員對象執行個體都是預定義的,是以我們通過枚舉來實作單例模式非常的便捷。

單例模式 - 隻有一個執行個體

類加載的時候并不會執行個體化 singleton5,而是在第一次調用 getinstance() 加載内部類 sigletonholder,此時才進行初始化 instance 成員變量,確定記憶體中的對象唯一性。

單例模式 - 隻有一個執行個體

假設,我們現在有一個計數類 counter 用來統計累加次數,每次調用 plus() 方法會進行累加。

單例模式 - 隻有一個執行個體

這個案例的實作方式會生成多個執行個體,那麼我們如何使用單例模式確定隻生成一個執行個體對象呢?

實際上,拆解成3個步驟就可以實作我的需求:靜态類成員變量、私有的構造方法、全局通路方法。

單例模式 - 隻有一個執行個體

基于單例模式,我們還可以進行擴充改造,擷取指定個數的對象執行個體,節省系統資源,并解決單例對象共享過多有性能損耗的問題。

我們來做個練習,我現在有一個需求,希望實作最多隻能生成 2 個 resource 類的執行個體,可以通過 getinstance() 方法進行通路。

單例模式 - 隻有一個執行個體

如果認為單例模式是非靜态方法。而靜态方法和非靜态方法,最大的差別在于是否常駐記憶體,實際上是不對的。它們都是在第一次加載後就常駐記憶體,是以方法本身在記憶體裡,沒有什麼差別,是以也就不存在靜态方法常駐記憶體,非靜态方法隻有使用的時候才配置設定記憶體的結論。

是以,我們要從場景的層面來剖析這個問題。如果一個方法和他所在類的執行個體對象無關,僅僅提供全局通路的方法,這種情況考慮使用靜态類,例如 java.lang.math。而使用單例模式更加符合面向對象思想,可以通過繼承和多态擴充基類。此外,上面的案子中,單例模式還可以進行延伸,對執行個體的建立有更自由的控制。

資料庫連接配接并不是單例的,如果一個系統中隻有一個資料庫連接配接執行個體,那麼全部資料通路都使用這個連接配接執行個體,那麼這個設計肯定導緻性能缺陷。事實上,我們通過單例模式確定資料庫連接配接池隻有一個執行個體存在,通過這個唯一的連接配接池執行個體配置設定 connection 對象。

如果采用餓漢式,在類被加載時就執行個體化,是以無須考慮多線程安全問題,并且對象一開始就得以建立,性能方面要優于懶漢式。

如果采用懶漢式,采用延遲加載,在第一次調用 getinstance() 方法時才執行個體化。好處在于無須一直占用系統資源,在需要的時候再進行加載執行個體。但是,要特别注意多線程安全問題,我們需要考慮使用雙重校驗鎖的方案進行優化。

實際上,我們應該采用餓漢式還是采用懶漢式,取決于我們希望空間換取時間,還是時間換取空間的抉擇問題。

此外,枚舉和靜态内部類也是非常不錯的實作方式。