概述:確保一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。
關鍵點:
- 構造函數不對外開放,一般為private。
- 通過一個靜態方法或者枚舉返回單例類對象。
- 確保單例類的對象有且只有一個,尤其在多線程情況下。
- 確保單例類對象在反序列化時不會重新構建對象
(1)餓漢模式
餓漢式單例模式(在類加載時就完成了初始化,所以類加載較慢,但獲取對象的速度快)
public class EagerSingle { //餓漢模式單例 //在類加載時就完成了初始化,所以類加載較慢,但獲取對象的速度快 private static EagerSingle single = new EagerSingle();//靜態私有成員,已初始化 private EagerSingle() { //私有構造函數 } public static EagerSingle getInstance() {//靜態,不用同步(類加載時已初始化,不會有多線程的問題) return single; } }
(2)懶漢模式
懶漢模式聲明一個靜態對象,並且在用戶第一次調用getInstance時進行初始化。
public class LazySingleton { //懶漢模式單例 //比較懶,在類加載時不創建實例,因此類加載熟讀快,但運行時獲取對象速度慢 private static LazySingleton instance;//靜態私有成員,沒有初始化 private LazySingleton() { //私有構造函數 } public static synchronized LazySingleton getInstance() {//靜態、同步、公開訪問 if (instance == null) { instance = new LazySingleton(); } return instance; } }
synchronized關鍵字保證了同步,在多線程情況下單例的唯一性。
存在問題:即使instance已經存在,每次調用getInstance依然會進行同步,這樣就會消耗不必要的資源。
總結:懶漢模式的優點是只有在使用時才會實例化單例對象,在一定程度上節約了資源;缺點是第一次加載時需要進行實例化,反應稍慢;最大問題是每次調用都會進行同步嗎,造成不必要的同步開銷。這種模式一般不建議使用。
(3)Double Check Lock(DCL)雙重校驗鎖
DCL方式實現單例的優點是既能在需要時才初始化單例,又能保證線程安全,且單例對象初始化后調用getInstance不進行同步鎖。
public class DCLSingleton { //Double Check Lock單例模式 //懶漢模式的改進 //但仍然存在隱患 private static DCLSingleton instance = null; private DCLSingleton() { } public static DCLSingleton getInstance() { if (instance == null) {//第一層判斷主要是為了避免不必要的同步 synchronized (DCLSingleton.class) { if (instance == null) {//第二層判空是為了在null情況下創建實例 instance = new DCLSingleton(); } } } return instance; } }
亮點在getInstance方法上,有兩次判空。第一層判斷主要是為了避免不必要的同步,第二層判空是為了在null情況下創建實例。
- 執行下面這行代碼
single = new Singleton();
實際上並不是一個原子操作,這句代碼實際做了3件事
- 給Singleton的實例分配內存;
- 調用Singleton()的構造函數,初始化成員字段
- 將instance對象指向分配的內存空間(此時instance已經不是null了)
問題:但由於java編譯器允許處理器亂序執行,上述順序2、3是不能保證的,可能是1-2-3也可能是1-3-2;如果是后者,3執行了已經非空,再走2會出現問題,這就是DCL失效。
解決: volatile關鍵字
// private static DCLSingleton instance = null; private volatile static DCLSingleton instance = null;
只需要加上volatile關鍵字,如上述代碼操作就可以保證instance對象每次都是從主內存中讀取的,就可以采用DCL來完成單例模式了。當然,volatile或多或少會影響到性能,但考慮到程序的正確性,犧牲點性能還是值得的。
總結:
- 優點:資源利用率高,第一次執行getInstance時單例對象才會被實例化,效率高。
- 缺點:第一次加載時反應稍慢;由於java內存模型的原因偶爾會失敗,在高並發環境下也有一定的缺陷,雖然概率很小。
- DCL模式是使用最多的單例實現方式
(4)靜態內部類單例模式
public class InnerSingleton { private InnerSingleton() { } public static InnerSingleton getInstance() { return InnerSingletonHolder.instance; } /** * 靜態內部類 */ private static class InnerSingletonHolder { private static final InnerSingleton instance = new InnerSingleton(); } }
總結:第一次加載InnerSingleton類時並不會初始化instance,只有在第一次調用InnerSingleton的getInstance方法時才會導致instance被初始化。因此,第一次調用getInstance方法會導致虛擬機加載InnerSingleton類,這種方法不僅能保證線程安全,也能夠保證單例對象的唯一性,同時也延遲了單例的實例化,所以這也是一種推薦的單例模式實現方法
(5)枚舉單例
public enum EnumSingleton { INSTANCE; public void doSomething() { //do sth ... } }
震驚?沒錯!就是枚舉!
- 寫法簡單;枚舉在java種與普通類是一樣的,不僅能夠有字段,還能夠有自己的方法。最重要的是默認枚舉實例的創建時線程安全的,並且在任何情況下都是一個單例。
- 為什么這么說呢? 在上述的集中單例模式實現種,在一個情況下他們都會出現重新創建對象的情況,那就是反序列化。
補充: 通過序列化可以將一個單例的實例對象寫到磁盤,然后再讀回來,從而有效的獲取一個實例。即使構造函數是私有的,反序列化時依然可以通過特殊的途徑去創建類的一個新的實例,相當於調用該類的構造函數。