設計模式是前人工作的總結和提煉。通常,被人們廣泛流傳的設計模式都是對某一特定問題的成熟的解決方案。如果能合理的使用設計模式,不僅能使系統更容易被他人了解,同時也能使系統擁有更加合理的結構。本節總結歸納了一些經典的設計模式,并詳細說明它們與軟體性能之間的關系。
單例模式是設計模式中使用最為普遍的模式之一。它是一種對象建立模式,用于生産一個對象的具體實作,它可以確定系統中一個類隻産生一個執行個體。在java語言中,這樣的行為能帶來兩大好處:
(1)對于頻繁使用的對象,可以省略建立對象所花費的時間,這對于那些重量級對象而言,是非常可觀的一筆系統開銷。
(2)由于new操作的次數減少,因而對系統記憶體的使用頻率也會降低,這将減輕gc壓力,縮短gc停頓時間。
是以對于系統的關鍵元件和被頻繁使用的對象,使用單例模式便可以有效地改善系統的性能。
單例模式的參與者非常簡單,隻有單例類和使用者兩個,如圖2.1所示。
表2.1 單例模式角色
角色
作用
單例類
提供單例的工廠,傳回單例
使用者
擷取并使用單例類
它的基本結構如圖2.1所示。
圖2.1 單例模式類圖
單例模式的核心在于通過一個接口傳回唯一的對象執行個體。一個簡單的單例實作如下:
注意代碼中的重點标注部分,首先單例類必須要有一個private通路級别的構造函數,隻有這樣,才能確定單例不會在系統中的其他代碼内被執行個體化,這點是相當重要的;其次,instance 成員變量和 getinstance() 方法必須是 static 的。
ps:單例模式是非常常用的一種結構,幾乎所有的系統中都可以找到它的身影。是以,希望讀者可以通過本節,了解單例模式的幾種實作方法及其各自的特點。
這種單例的實作方式非常簡單,而且非常可靠。它唯一的不足僅是無法對 instance 執行個體做延遲加載。假如單例的建立過程很慢,而由于 instance 成員變量是 static 定義的,是以在 jvm 加載單例類時,單例對象就會被建立,如果此時,這個單例類在系統中還扮演其他角色,那麼在任何使用這個單例類的地方都會初始化這個單例變量,而不管是否會被用到,比如單例類作為string工廠,用于建立一些字元串(該類既用于建立單例singleton,又用于建立string對象):
當使用singleton.createstring()執行任務時,程式輸出:
singleton is create
createstring in singleton
可以看到,雖然此時并沒有使用單例類,但它還是被建立出來,這也許是開發人員所不願意看到的,為了解決這個問題,并以此提高系統在相關函數調用時的反應速度,就需要引入延遲加載機制。
首先,對于靜态成員變量 instance 初始值為 null ,確定系統啟動時沒有額外的負載,其次,在getinstance() 工廠方法中,判斷目前單例是否已經存在,若存在則傳回,不存在則再建立單例。這裡尤其還要注意,getinstance()方法必須是同步的,否則在多線程環境下,當線程1正建立單例時,完成指派操作前,線程2可能判斷 instance 為 null ,故線程2也将啟動建立單例的程式,而導緻多個執行個體被建立,故同步關鍵字是必須的。
使用上例中的單例實作,雖然實作了延遲加載的功能,但和第一種方法相比,它引入了同步關鍵字,是以在多線程的環境中,它的時耗要遠遠大于第一種單例模式。以下測試代碼就說明了這個問題:
開啟五個線程同時完成以上代碼的運作,使用第一種類型的單例耗時0ms,而是用lazysingleton 卻相對耗時約390ms。性能至少相差兩個等級。
ps:在本書中,會使用很多類似代碼片段用于調試不同代碼的執行速度。在不同的計算機上其測試結果很可能與筆者不同。讀者大可不必關心測試資料的絕對值,隻要觀察用于比較的目标代碼間的相對耗時即可。
為了使用延遲加載引入的同步關鍵字反而降低了系統性能,是不是有點得不償失呢? 為了解決這個問題,還需要 對其他進行改進。
在這個實作中,單例模式使用内部類來維護單例的執行個體,當staticsingleton 被加載時,其内部類并不會别初始化,故可以確定staticsingleton 類被載入jvm時,不會初始化這個單例類,而當getsingleton() 方法被調用時,才會加載 singletonholder ,進而初始化 instance 。 同時,由于執行個體的建立是在類加載時完成,故天生對多線程友好, getinstance() 方法也不需要使用同步關鍵字。是以,這種實作方式同時具備以上兩種實作的優點。
ps:使用内部類的方式實作單例,既可以做到延遲加載,也不必使用同步關鍵字,是一種比較完善的實作。
通常情況下,用以上方式實作的單例已經可以確定在系統中隻存在唯一執行個體了。但仍然有例外情況,可能導緻系統生成多個執行個體,比如在代碼中,通過反射機制,強行調用單例類的私有構造函數,生成多個單例,考慮到情況的特殊性,本書中不對這種極端的方式進行讨論。但仍有些合法的方法,可能導緻系統出現多個單例類的執行個體。
一個可以被串行的執行個體:
測試代碼如下:
使用一段測試代碼測試單例的串行化和反串行化,當去掉 sersingleton 代碼中加粗的 readreslove() 函數時,以下測試代碼抛出異常:
說明測試代碼中 s 和 s1 指向了不同的執行個體,在反序列化後生成了多個對象執行個體,而加上 readreslove() 函數的,程式正常退出。說明,即使經過反序列,仍然保持了單例的特征。事實上,在實作了私有的readreslove() 方法後,readobject() 已經形同虛設,它直接使用 readreslove() 替換了原本的傳回值,進而在形式上構造了單例。
ps:序列化和反序列化可能會破壞單例。一般來說,對單例進行序列化和反序列化的場景并不多見,但如果存在,就要多加注意。