设计模式之单例模式:7种单例设计模式(Java实现)
設計模式之單例模式:7種單例設計模式(Java實現)
- 單例模式
- 餓漢式
- 懶漢式
- 懶漢式+同步方法
- Double-Check
- Double-Check+Volatile
- Holder方式
- 枚舉方式
- 各種單例模式的對比與總結
在經歷了秋招的多場面試的毒打之后,我發現Java后端開發方向的許多面試官對設計模式都挺感興趣。最近結合著汪文君老師的《Java高并發編程詳解》書籍,對設計模式進行了一些學習,本博客主要記錄一下對于單例模式的學習心得。
單例模式
單例模式是GoF23種設計模式中最常用的設計模式之一,它在日常生活中的應用有任務管理器、回收站等,特點是只能打開一個。單例模式提供了一種在多線程情況下保證實例唯一性的解決方案。
本博客記錄七種單例模式的實現方法,分別是:(1)餓漢式;(2)懶漢式;(3)懶漢式+同步方法;(4)Double-Check;(5)Double-Check + Volatile;(6)Holder方法;(7)枚舉方式。
為了比較這七種方式,我們可以從線程安全、高性能和懶加載這三個維度去衡量。
餓漢式
餓漢式的單例模式的代碼如下:
package Chapter14.Hungry;// 餓漢式的單例模式的設計 // final不允許被繼承 public final class Singleton {// 實例變量private byte[] data = new byte[1024];// 在定義實例對象的時候直接初始化private static Singleton instance = new Singleton();// 私有構造函數,不允許外部newprivate Singleton() {}public static Singleton getInstance() {return instance;} }其中,Singleton類是使用final修飾的,不允許被繼承。data是該類的一個實例變量,占用1KB的空間。接下來的其他方式中也會存在相似的部分。
實現單例模式的重點在于:instance對象、私有構造函數和獲取instance對象的方法。下面具體分析一下:
在餓漢式的單例模式下,如果主動使用了Singleton類,instance在類加載的初始化階段就會直接完成創建,因此該方法在多線程的情況下也能保證同步,因為不可能被實例化兩次,不存在線程不安全的情況。
如果一個類的成員屬性比較少,且占用的內存資源不多,餓漢式的性能是比較高的(因為getInstance()方法非常簡單);否則,餓漢式存在的缺點就被放大,餓漢式也不是較優的選擇了。
餓漢式的主要缺點,就是餓漢式不能提供懶加載,在使用Singleton類時就創建了成員屬性,一直占用著內存資源。我們更希望能實現懶加載的方式,創建類時可以沒有data這些實例變量,直到使用getInstance()方法時再創建。
懶漢式
懶漢式的單例模式的代碼如下:
package Chapter14.Lazy;public final class Singleton {private byte[] data = new byte[1024];private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;} }對比一下餓漢式,我們可以發現懶漢式的主要變動就是,在類加載時,instance對象為空,直到getInstance()方法被調用,判斷instance是否仍然為空,若為空才進行賦值。這種區別也體現在“餓漢式”與“懶漢式”的名稱的區別上,“餓漢”看到東西就會去吃(類加載時就賦值instance),“懶漢”總是把事情拖到最后一刻(調用了getInstance()方法才去賦值instance)。
懶漢式的效能還是比較高的,也可以實現懶加載。但是上述懶漢式的代碼并不能保證線程同步。問題出在getInstance()方法中:原instance為null,如果兩個線程A和B,A調用getInstance(),執行instance = new Singletion()需要一定時間;在這段時間中,B也調用了getInstance(),也要去執行instance = new Singletion(),這樣instance就被實例化了兩次,線程不同步了。
懶漢式+同步方法
為了解決上述懶漢式設計模式中存在的問題,可以強制進行同步,即使用synchronized關鍵字去修飾getInstance()方法。代碼如下:
package Chapter14.LazySynchronized;public final class Singleton {private byte[] data = new byte[1024];private static Singleton instance = null;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;} }實際上就是在getInstance()方法前添加了synchronized關鍵字進行修飾。如此,同步的懶漢式可以保證線程安全,也可以實現懶加載。但是synchronized關鍵字天生的排他性導致了getInstance()方法在同一時刻只能被一個線程所訪問,性能低下。
Double-Check
Double-Check在懶漢式的基礎上,提供了一種高效的數據同步策略——首次初始化時加鎖,之后則允許多個線程同步調用getInstance()方法獲得實例。具體代碼如下:
package Chapter14.DoubleCheck;import java.net.Socket; import java.sql.Connection;public final class Singleton {private byte[] data = new byte[1024];private static Singleton instance = null;// conn和socket模擬其他需要初始化的實例變量Connection conn;Socket socket;private Singleton() {// 此處需要實例化conn和socket等實例變量}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}}在這里我們設置了conn和socket實例變量,用來模擬單例模式中一些需要進行初始化設置的實例變量,比如Connection conn需要與數據庫進行連接,Socket socket需要建立一個TCP連接等。接下來將會提到Double-Check的問題,就會和這些實例變量有關。
分析一下Double-Check如何實現線程安全:當兩個線程A和B同時發現instance == null時,只有線程A有資格進入同步代碼塊,完成對instance的實例化;等到A釋放同步資源后,線程B發現instance != null,就只需要去獲取instance即可。
因此Double-Check方式可以保證線程安全,也很高效,同時也支持懶加載。但是Double-Check有個致命的問題,那就是可能會引發空指針異常。下面我們來分析一下為什么會發生異常。
在Singleton構造函數中,我們需要實例化instance本身,同時還要實例化conn和socket這兩個資源。假設線程A第一個調用getInstance()方法,此時instance == null,因此A獲取到同步資源,對instance進行加載。由于指令會被重排序,在Singleton的構造函數中,有可能instance最先被實例化,而conn和socket等資源后被實例化。在conn和socket被實例化的過程中,又有一個線程B調用了getInstance()方法,它發現instance != null,則獲取到了instance,然后又去使用conn或socket,就會產生空指針異常。
Double-Check+Volatile
既然我們分析到了,產生異常的原因在于指令可能會被重排序,導致instance被實例化早于其他實例資源。因此我們可以用volatile關鍵字去修飾instance,并嚴格管理Singleton構造函數中各個資源與instance的順序。具體代碼如下:
package Chapter14.DoubleCheckVolatile;import java.net.Socket; import java.sql.Connection;public final class Singleton {private byte[] data = new byte[1024];// 加volatile關鍵字保證有序性,防止指令重排序private volatile static Singleton instance = null;Connection conn;Socket socket;private Singleton() {// 先實例化conn和socket等實例變量// 最后實例化instance}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;} }由此,我們實現了一種滿足線程安全、高性能、懶加載的單例模式。不過這種方式用得還是比較少,更常見的是下面兩種單例模式。
Holder方式
Holder方式的單例模式的代碼如下:
package Chapter14.Holder;public final class Singleton {private byte[] data = new byte[1024];private Singleton() {}// 在靜態內部類中持有Singleton的實例,并且可被直接初始化private static class Holder {private static Singleton instance = new Singleton();}// 調用getInstance()方法,實際上是獲得Holder的instance靜態屬性public static Singleton getInstance() {return Holder.instance;} }Holder方式完全借助了類加載的特性。在Singleton類中并沒有instance的靜態成員變量,只有在其靜態內部類Holder中有。因此,在Singleton類初始化時,并不會創建Singleton的實例。只有在getInstance()方法第一次被調用時,Singleton的實例instance才會被創建,由此實現了懶加載。另外,這個創建過程是在Java編譯時期收集至<clinit>()方法(JVM的一個內部方法,在類加載的初始化階段起作用)中的。這個方法是同步方法,保證內存的可見性、JVM指令的原子性和有序性。
因此Holder方式可以保證線程安全、高性能和懶加載,它也是單例模式中最好的設計之一,使用非常廣泛。
枚舉方式
枚舉方式是《Effective Java》作者力推的方式,在很多優秀開源代碼中經常可以看到枚舉方式的例子。枚舉方式的示例代碼如下:
package Chapter14.Enum;public enum Singleton {INSTANCE;private byte[] data = new byte[1024];Singleton() {System.out.println("INSTANCE will br initialized immediately");}public static void method() {// 調用該方法將會主動使用Singleton, INSTANCE將會被初始化}public static Singleton getInstance() {return INSTANCE;}}枚舉方式不能被繼承,只能實例化一次,同樣也是線程安全,且高效的。但是枚舉方式不能懶加載,第一次調用Singleton枚舉時,instance就會被實例化。
可以根據Holder方式,對枚舉方式進行改造,改造后的代碼如下:
各種單例模式的對比與總結
下表展示了各種單例模式在線程安全、高性能、懶加載維度的一些比較:
| 餓漢式 | √ | √ | × | |
| 懶漢式 | × | √ | √ | |
| 懶漢式+同步方法 | √ | × | √ | |
| Double-Check | √ | √ | √ | 會引發空指針異常 |
| Double-Check+Volatile | √ | √ | √ | |
| Holder方式 | √ | √ | √ | |
| 枚舉方式 | √ | √ | × | 可以添加Holder方式實現懶加載 |
| 枚舉方式+Holder方式 | √ | √ | √ |
最后,《Java高并發編程詳解》的作者汪文君老師指出,在實際開發中,Holder方式和枚舉方式是最常見的設計單例的方式。
我們可以多多學習、應用,最終完全掌握單例模式,并進一步學習其他的設計模式。在開發中,各種設計模式還是很重要的。
總結
以上是生活随笔為你收集整理的设计模式之单例模式:7种单例设计模式(Java实现)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java web application
- 下一篇: subversion安装与配置备忘录