一、什么是單例模式
大家學操作系統的時候應該知道,當多個進程或線程同時操作一個文件時,只有一個能訪問;java中類似的例子也有很多,比如多線程中我們最常用的鎖,保證了多線程同時對一個方法或對象操作時只有一個能夠訪問。單例模式就是如此,我們給出它的定義。
單例模式(Singleton),保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。UML結構圖如下:

其中,Singleton類定義了一個getInstance操作,允許客戶端訪問它的唯一實例,getInstance是一個靜態方法,主要負責創建自己的唯一實例。而對象的聲明是private的,其他類無法訪問到,只能通過getInstance()方法訪問其唯一實例。
1 public class Singleton { 2 3 private static Singleton instance; 4 5 //限制產生多個對象 6 private Singleton() { 7 } 8 9 //通過方法獲取實例對象 10 public static Singleton getInstance() { 11 if(instance == null) { 12 instance = new Singleton(); 13 } 14 15 return instance; 16 } 17 18 }
上述代碼就是一個單例模式,首先聲明了靜態私有的一個對象,並通過getInstance方法返回該對象。如果該對象已經存在,則直接返回該對象,若不存在,則實例化后返回該對象。下面看一段代碼:
public class Client { public static void main(String[] args) { Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); if(instance1 == instance2) { System.out.println("兩個對象是相同的實例"); } } }
二、單例模式的應用
1. 何時使用
- 當我們想控制實例數目,節省系統資源時,可以使用單例模式。
2. 優點
- 內存中只有一個實例,減少了內存開支,尤其一個對象需要頻繁地創建銷毀,而此時性能又無法優化時,單例模式的優勢就非常明顯。
- 避免對資源的多重占用(比如寫文件操作,只有一個實例時,避免對同一個資源文件同時寫操作),簡單來說就是對唯一實例的受控訪問。
3. 缺點
- 沒有接口,不能繼承,與單一職責沖突。
4. 使用場景
- 要求生成唯一序列號的環境。
- 在整個項目中有一個共享訪問點或共享數據(如web頁面上的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來即可)。
- 創建一個對象需要消耗的資源過多時(如訪問I/O和數據庫等資源)。
5. 應用實例
- 一個黨只有一個主席/一個國家只有一個國王/一個皇朝只有一個皇帝。
- 計划生育。
- 多個進程或線程同時操作一個文件的現象。
三、高並發下的單例模式
需要注意的是,在高並發情況下,要注意單例模式線程同步的問題。單例模式有幾種不同的實現方式,如上方的代碼就需要考慮線程同步。
1. 懶漢式
所謂懶漢式單例,就是通過在上述代碼中增加synchronized關鍵字來實現。
public class Singleton { private volatile static Singleton instance; private static Object syncRoot = new Object(); private Singleton() { } public static Singleton getInstance() { //雙重鎖定 if(instance == null) { synchronized (syncRoot) { if(instance == null) { instance = new Singleton(); } } } return instance; } }
這里使用了雙重鎖定(Double-Check Locking),這樣可以不用讓線程每次都加鎖,而只是在實例未被創建的時候再枷鎖處理,同時也能保證多線程的安全。
而這里判斷了兩次instance實例是否存在的原因是,當instance為null時,並且同時有兩個線程調用getInstance()方法時,它們都可以通過第一重instance==null的判斷,然后由於lock機制,這兩個線程則只有一個進入,另一個在外排隊等候,必須要其中一個進入並出來后,另一個才能進入,而此時如果沒有了第二重排序,則第一個線程創建了實例,而第二個線程還是可以繼續再創建新的實例,就沒有達到單例的目的。
這里還需要注意一個問題,第三行中加入了volatile關鍵字,這里如果不加volatile可能會出現一個錯誤,即當代碼讀取到第11行的判斷語句時,如果instance不為null時,instance引用的對象有可能還沒有完成初始化,線程將訪問到一個還未初始化的對象。究其原因是因為代碼第14行,創建了一個對象,此代碼可分解為三行偽代碼,即分配對象的內存空間、初始化對象、設置instance指向剛分配的內存地址,分別記為1、2、3,在2和3之間,可能會被重排序,重排序后初始化就變為了最后一步。因此,線程A的intra-thread semantics(所有線程在執行Java程序時必須遵守intra-thread semantics,它保證重排序不會改變單線程內的程序執行結果)沒有改變,但A2和A3的重排序將導致線程B判斷出instance不為空,線程B接下來將訪問instance引用的對象,此時,線程B將會訪問到一個還未初始化的對象。而使用volatile就可以實現線程安全的延遲初始化,本質時通過禁止2和3之間的重排序,來保證線程安全的延遲初始化。
2. 餓漢式
餓漢式單例就不會出現產生多個單例的情況,但它是在自己被加載時就將自己實例化,所以要提前占用系統資源。
public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } //類中其他方法,盡量使static public static void dosomething() { } }
3. 靜態內部類
這種方式與餓漢式一樣,同樣利用了類加載來保證只創建一個instance實例,因此不存在線程安全的問題,不一樣的是,它是在內部類里面去創建對象實例。這樣只要應用中不使用內部類,JVM就不會去加載這個單例類,也就不會創建單例對象,從而實現懶漢式延遲加載。
public class Singleton { //靜態內部類 private static class SingletonHolder { public static Singleton instance = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return SingletonHolder.instance; } }
4. 枚舉
上面實現單例的方式都需要額外的工作來實現序列化,而且可以使用反射強行調用私有構造器。
而枚舉很好的解決了這兩個問題,使用枚舉除了線程安全和防止反射調用構造器外,還提供了自動序列化機制,防止反序列化的時候創建新的對象。
public enum Singleton { instance; public static void dosomething() { } }
枚舉的客戶端寫法如下:
public class Client { public static void main(String[] args) { //枚舉 Singleton instance1 = Singleton.instance; Singleton instance2 = Singleton.instance; if(instance1 == instance2) { System.out.println("兩個對象是相同的實例"); } } }
四、單例模式的實現
下面舉一個完整的例子,就以一個皇朝只有一個皇帝為例,假設當今是唐朝小李的天下,來看看怎么用單例模式實現。UML圖如下:

1. 皇帝類(Emperor類)
public class Emperor { private static final Emperor EMPEROR = new Emperor(); private Emperor() { } public static Emperor getEmperor() { return EMPEROR; } public static void say() { System.out.println("朕乃當今聖上小李"); } }
這里使用的是餓漢式單例,這樣我們在加載類的時候就對對象進行了實例化操作,后續只需調用getEmperor()方法即可。
2. 客戶端
public class Client { public static void main(String[] args) { //臣子朝拜 for(int day=0; day<3 ;day++) { Emperor emperor = Emperor.getEmperor(); emperor.say(); } } }
客戶端部分寫了一個每日早朝的情況,臣子每日都要朝拜皇帝,今天朝拜的皇帝應該和昨天、前天的一樣,運行結果如下:

運行結果表示,連續三天上朝的皇帝都是小李,這就是一個簡單的單例模式。
