點擊上方 [Java隨筆錄]
關注可了解更多Java相關的技術分享。問題或建議,歡迎公眾號留言!
單例模式可以說是設計模式中最簡單的一個了,它屬於創建型模式,其定義如下:
單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。
在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個伺服器程序中,該伺服器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。
實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱);當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用;同時我們還將該類的構造函數定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。單例模式的類圖如下所示:
單例模式的應用場景對於Java來說,單例模式可以保證在一個 JVM 中只存在單一實例。單例模式的應用場景主要有以下幾個方面;
需要頻繁創建的一些類,使用單例可以降低系統的內存壓力,減少GC;
某類只要求生成一個對象的時候,如一個班中的班長、每個人的身份證號等;
某些類創建實例時佔用資源較多,或實例化耗時較長,且經常使用;
某類需要頻繁實例化,而創建的對象又頻繁被銷毀的時候,如多線程的線程池、網絡連接池等;
頻繁訪問資料庫或文件的對象;
對於一些控制硬體級別的操作,或者從系統上來講應當是單一控制邏輯的操作,如果有多個實例,則系統會完全亂套;
當對象需要被共享的場合。由於單例模式只允許創建一個對象,共享該對象可以節省內存,並加快對象訪問速度。如Web中的配置對象、資料庫的連接池等。
單例模式的優缺點單例模式的優點:
單例模式的缺點:
單例模式一般沒有接口,擴展困難。如果要擴展,則除了修改原來的代碼,沒有第二種途徑,違背開閉原則;
在並發測試中,單例模式不利於代碼調試。在調試過程中,如果單例中的代碼沒有執行完,也不能模擬生成一個新的對象;
與單一職責原則衝突,一個類只應該關心其內部邏輯的實現,而不應該插手外部對其的實例化。
代碼實現單例模式的代碼實現其實很簡單,要做到一個類的實例只能由類自身進行創建,關鍵就是私有化其構造函數。其中,單例模式在多線程的應用場合下必須小心使用。如果當唯一實例尚未創建時,有兩個線程同時調用創建方法,那麼它們同時沒有檢測到唯一實例的存在,從而同時各自創建了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例唯一的原則。結合不同的場景需求(單線程多線程)以及Java的特性(鎖和類加載),就會有很多種實現方法,我這裡總結了常見的五種。
(1) 餓漢式餓漢式實現起來簡單,它基於類加載機制,不用加鎖也能避免多線程同步的問題,所以執行效率會比較高。但是這種方法有個缺點,就是在類加載的時候就會進行實例化,沒有達到Lazy Loading的效果。該種方法的代碼實現如下:
1public class Singleton {
2
3 private Singleton() {
4
5 }
6
7 private static final Singleton instance = new Singleton();
8
9 public static Singleton getInstance() {
10 return instance;
11 }
12
13}
針對餓漢式中沒有Lazy Loading的效果,可以通過如下方法來實現。不過該方法也有個最大的問題,就是只能支持在單線程環境下工作,它是線程不安全的,多線程的話會出現多個實例化對象的出現。具體代碼如下:
1public class Singleton {
2
3 private Singleton() {
4
5 }
6
7 private static Singleton instance;
8
9 public static Singleton getInstance() {
10 if (instance == null) {
11 instance = new Singleton();
12 }
13 return instance;
14 }
15
16}
針對線程不安全的問題,最簡單直接的解決方法就是加鎖,對getInstance( )方法加鎖,這能很快的解決多線程安全問題,但是帶來的問題也很明顯,就是太影響性能了。具體代碼如下所示:
1public class Singleton {
2
3 private Singleton() {
4
5 }
6
7 private static Singleton instance;
8
9 synchronized public static Singleton getInstance() {
10 if (instance == null) {
11 instance = new Singleton();
12 }
13 return instance;
14 }
15
16}
直接對整個getInstance( )方法上鎖,能夠解決線程安全問題,但會大大影響程序的性能,因為很容易出現鎖競爭的情況。這裡使用雙重校驗的機制,一方面能夠解決線程安全問題,另一方面還能保持高性能。關鍵就是在於兩個if判斷,其實大部分情況下外部的if判斷是不會通過的,因為只要該單例類被實例化了一次,外部if的判斷條件就成立不了了,所以能夠保持較好的性能;而內部的if判斷,則針對的是單例類還沒被實例化的時候,可能有多個線程同時通過了外部的if判斷,要對單例類進行實例化,此時就需要上鎖,並在臨界區內再次判斷實例是否已經被創建,防止多次創建,因此內部的if判斷保證了只有一個實例會被創建。具體代碼如下:
1public class Singleton {
2
3 private Singleton() {
4
5 }
6
7 private static volatile Singleton instance;
8
9 public static Singleton getInstance() {
10 if (instance == null) {
11 synchronized (Singleton.class) {
12 if (instance == null) {
13 instance = new Singleton();
14 }
15 }
16 }
17 return instance;
18 }
19
20}
該方法能夠達到同雙重校驗方式相同的效果,且實現方法更為簡單,不需要藉助鎖就能實現。主要是藉助了類加載的特性,內部靜態類只有在使用的時候才會進行加載,這就能夠實現Lazy Loading;且類加載時只會加載一次,天然的線程安全保證。具體代碼如下:
1public class Singleton {
2
3 private Singleton() {
4
5 }
6
7 private static class InnerClass {
8 private static final Singleton instance = new Singleton();
9 }
10
11 public static Singleton getInstance() {
12 return InnerClass.instance;
13 }
14
15}