一、單例模式概念
單例模式確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項,這個類稱為單例類,它提供了全域性訪問的方法。
單例模式的三個特點:
- 這個類只能有一個例項。
- 這個類自行建立這個例項。
- 這個類自行向整個系統提供這個例項。
單例模式的應用有,Windows 裡的工作管理員(一個系統中只有一個)、網站計數器(實現多個頁面的計數同步)、資料庫連線池(減少資源損耗)等。
二、單例模式實現
根據單例模式的特點,我們來實現單例模式,在類中提供一個靜態方法來獲取這個唯一的例項物件,給其他類提供例項,並且這個例項物件不能直接使用 new 建立,所以構造方法要宣告成私有,這便是最簡單的單例模式實現。這樣的實現在單執行緒環境中,當然沒問題,但是我們還要考慮多執行緒環境下的安全實現。
下面是對單例模式的各種實現,並且對每種實現方法都在多執行緒環境中做了測試,所有程式碼都在我的 GitHub 倉庫中中,傳送門。該倉庫還在完成中,用 Java 實現 23 種設計模式,並對設計原則和每種設計模式做出詳細的分析,感興趣的可以 fork 或者 star 哦,也歡迎小夥伴參與該倉庫的完成。
2.1 懶漢式單例類(執行緒不安全)
通過 getInstance() 方法得到單例物件,單例物件在需要的時候才被延遲建立,所以稱之為懶漢式。但是在多執行緒環境中,由於這個 getInstance() 方法可能被多個執行緒同時呼叫,這很可能會建立多個例項,所以這種實現在多執行緒環境下是不安全的。
2.2 懶漢式單例類(執行緒安全)
給 getInstance() 加上 synchronized 關鍵字後,可以保證這個方法在同一時間只能被一個執行緒呼叫,多個執行緒呼叫這個方法要排隊依次呼叫,這就保證了只會建立一個單例物件,在多執行緒環境下是安全的。
2.3 餓漢式單例類
相比於上面的懶漢式,餓漢式在類載入的時候就會建立例項物件,在 getInstance() 方法直接返回建立好的物件,簡單直接,在多執行緒環境下也是安全的。
2.4 雙重校驗鎖單例類
針對於上面的執行緒安全的懶漢式載入,這種實現方式不是直接給方法加上 synchronized 關鍵字,而是在 getInstance() 方法做雙重檢查來解決執行緒不安全的問題。這種方式允許多個執行緒同時呼叫該方法,但是在方法中會進行兩次檢查,第一次檢查例項是否已經存在,如果不存在才進入下面的同步程式碼塊,執行緒安全的建立例項,如果例項真的不存在(避免這是有其他執行緒建立好了,再次建立新的例項)才會建立例項。這種方式理論上要比直接使用 synchronized 關鍵字效能要高,但是對於不同虛擬機器對 volatile 關鍵字的優化,優勢並不明顯。
2.5 靜態內部類單例類
建立一個靜態內部類,來建立例項,和上面餓漢式相比,雖然都是直接 new 例項,但是這種方式在外部類載入時,靜態內部類並不會被載入。只有在第一次呼叫 getInstance() 方法時,才會顯式的載入靜態內部類,建立例項,也是一種延遲(懶)建立方式。
2.6 列舉單例類
列舉實現單例模式是 Java 大牛們比較推薦的,因為這種方式實現非常簡單,並且這種方式但是大多數單例模式的實現並不是這種方式。這種方式需要開發者對列舉有清晰的認識,這裡也簡單的回顧一下列舉的基本知識。
列舉是在 Java1.5 之後出現的,可以更加簡單的定義常量,通過反編譯,我們可以發現列舉其實也是一個 Java 類,這個類繼承自 Enum 介面,定義的列舉物件會被加上 static final 關鍵字,這就是我們不用列舉時宣告常量的方式,另外在 static 靜態程式碼塊中初始化列舉物件,列舉的構造方法被加上了 private 關鍵字,防止其他類建立新的列舉物件例項。雖然前面幾種方式無法直接使用 new 建立新的例項,但是可以用反射來繞過 private 限制,而列舉卻有自帶的序列化機制、防止反射攻擊造成多次例項化、執行緒安全的優點,從這些地方我們都可以看出使用列舉是實現單例模式的絕佳方式。
三、單例模式總結
根據對資源載入時機的需要,來選擇合適的單例模式實現方式,如果是懶載入方式,可以選擇懶漢方式和雙重校驗鎖方式;如果將資源載入的時間提前來達到使用時的快速體驗,可以選擇餓漢方式;如果涉及到序列化建立單例物件,可以選擇列舉方式。
單例模式的優點是提供了對唯一例項的訪問控制,可以節約系統資源,但缺點是單例類的職責過重,並且缺少抽象層難以擴充套件,不太符合單一職責原則。
想看更多程式設計文章,歡迎關注下方的微信公眾號哦。