單例模式的幾種實現方式及對比


所謂單例就是在系統中只有一個該類的實例。單例模式(Singleton),也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個全局的對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復雜環境下的配置管理。


單例模式實現有以下三個核心步驟:

  1. 構造方法私有化。即不能在該類之外實例化該類(不能再別處new該類),只能在類內實例化。
  2. 在本類中創建本類的實例。
  3. 在本類中提供給外部獲取實例的方式。

單例模式的實現方式有兩種:餓漢模式和懶漢模式。

1、餓漢模式

不管現在需不需要,先創建實例。關鍵在於“餓”,餓了就要立即吃。

1.1 靜態常量

這里將類的構造器私有化,就不能在外部通過new關鍵字創建該類的實例,然后定義了一個該類的常量,用static修飾,以便外部能夠獲得該類實例(通過HungryStaticConstantSingleton.INSTANCE 獲得)。也可以不加final關鍵字,具體看自己的需求。或者將INSTANCE私有化,並提供靜態方法getInstance返回INSTANCE實例。

 1 /**
 2  * 惡漢模式-靜態常量,簡潔直觀
 3  */
 4 public class HungryStaticConstantSingleton{
 5     //構造器私有化
 6     private HungryStaticConstantSingleton() {
 7     }
 8     //靜態變量保存實例變量 並提供給外部實例
 9     public final static HungryStaticConstantSingleton INSTANCE = new HungryStaticConstantSingleton();
10 }

 

優點

  • 由於使用了static關鍵字,保證了在引用這個變量時,關於這個變量的所有寫入操作都完成,所以保證了JVM層面的線程安全

缺點

  • 不能實現懶加載,造成空間浪費。如果一個類比較大,我們在初始化的時就加載了這個類,但是我們長時間沒有使用這個類,這就導致了內存空間的浪費。

1.2 枚舉

這種方式是最簡潔的,不需要考慮構造方法私有化。值得注意的是枚舉類不允許被繼承,因為枚舉類編譯后默認為final class,可防止被子類修改。常量類可被繼承修改、增加字段等,容易導致父類的不兼容。枚舉類型是線程安全的,並且只會裝載一次,設計者充分的利用了枚舉的這個特性來實現單例模式,枚舉的寫法非常簡單,而且枚舉類型是所有單例實現中唯一一種不會被破壞的單例實現模式

/**
 * 惡漢-枚舉形式,最簡潔
 */
public enum HungryEnumSingleton{
    INSTANCE;
    
    public void print(){
        System.out.println("這是通過枚舉獲得的實例");
        System.out.println("HungryEnumSingleton.pring()");
    }
}

 

Test,打印實例直接輸出了【INSTANCE】,是因為枚舉幫我們實現了toString,默認打印名稱。

public class EnumSingleton2Test{
    public static void main(String[] args) {
        HungryEnumSingleton singleton2 = HungryEnumSingleton.INSTANCE;
        System.out.println(singleton2);
        singleton2.print();
    }
}

 輸出結果

 

1.3 靜態代碼塊

這種方式和上面的靜態常量/變量類似,只不過把new放到了靜態代碼塊里,從簡潔程度上比不過第一種。但是把new放在static代碼塊有別的好處,那就是可以做一些別的操作,如初始化一些變量,從配置文件讀一些數據等。

/**
 * 惡漢模式-靜態代碼塊
 */
public class HungryStaticBlockSingleton{

    //構造器私有化
    private HungryStaticBlockSingleton() {
    }

    //靜態變量保存實例變量
    public static final HungryStaticBlockSingleton INSTANCE;

    static {
        INSTANCE = new HungryStaticBlockSingleton();
    }
}

 

如下,在static代碼塊里讀取 info.properties 配置文件動態配置的屬性,賦值給 info 字段。

/**
 * 惡漢模式-靜態代碼塊
 * 這種用於可以在靜態代碼塊進行一些初始化
 */
public class HungryStaticBlockSingleton{

    private String info;

    private HungryStaticBlockSingleton(String info) {
        this.info = info;
    }

    //構造器私有化
    private HungryStaticBlockSingleton() {
    }

    //靜態變量保存實例變量
    public static final HungryStaticBlockSingleton INSTANCE;

    static {
        Properties properties = new Properties();
        try {
            properties.load(HungryStaticBlockSingleton.class.getClassLoader().getResourceAsStream("info.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        INSTANCE = new HungryStaticBlockSingleton(properties.getProperty("info"));
    }
    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }
}

 

Test,

public class HungrySingletonTest{
    public static void main(String[] args) {
        HungryStaticBlockSingleton hun = HungryStaticBlockSingleton.INSTANCE;
        System.out.println(hun.getInfo());
    }
}

 

輸出

 

2、懶漢模式

需要時再創建,關鍵在於“懶”,類似懶加載。

2.1 非線程安全

同樣是構造方法私有化,提供給外部獲得實例的方法,getInstance()方法被調用時創建實例。該方式適用於單線程,因為在多線程的情況下可能會發生線程安全問題,導致創建不同實例的情況發生。可以看下面的演示。

 1 /**
 2  * 懶漢模式-線程不安全的,適用於單線程
 3  */
 4 public class LazyUnsafeSingleton{
 5     private LazyUnsafeSingleton(){
 6     }
 7     private static LazyUnsafeSingleton instance;
 8     public static LazyUnsafeSingleton getInstance(){
 9         if(instance==null){
10             instance = new LazyUnsafeSingleton();
11         }
12         return instance;
13     }
14 }

非線程安全演示

 1 public class LazyUnsafeSingletionTest{
 2     public static void main(String[] args) throws ExecutionException, InterruptedException {
 3         ExecutorService es = Executors.newFixedThreadPool(2);
 4         Callable<LazyUnsafeSingleton> c1 = new Callable<LazyUnsafeSingleton>(){
 5             @Override
 6             public LazyUnsafeSingleton call() throws Exception {
 7                 return LazyUnsafeSingleton.getInstance();
 8             }
 9         };
10         Callable<LazyUnsafeSingleton> c2 = new Callable<LazyUnsafeSingleton>(){
11             @Override
12             public LazyUnsafeSingleton call() throws Exception {
13                 return LazyUnsafeSingleton.getInstance();
14             }
15         };
16         Future<LazyUnsafeSingleton> submit = es.submit(c1);
17         Future<LazyUnsafeSingleton> submit1 = es.submit(c2);
18         LazyUnsafeSingleton lazyUnsafeSingleton = submit.get();
19         LazyUnsafeSingleton lazyUnsafeSingleton1 = submit1.get();
20         es.shutdown();
21 
22         System.out.println(lazyUnsafeSingleton);
23         System.out.println(lazyUnsafeSingleton);
24         System.out.println(lazyUnsafeSingleton1==lazyUnsafeSingleton);
25     }
26 }

 

輸出 大概運行三次就會出現一次,我們可以在 LazyUnsafeSingleton 中判斷 if(instance==null) 之后增加線程休眠以獲得更好的效果。

2.2 線程安全的雙重檢查鎖模式

該方式是懶漢模式中線程安全的創建方式。通過同步代碼塊控制並發創建實例。並且采用雙重檢驗,當兩個線程同時執行第一個判空時,都滿足的情況下,都會進來,然后去爭鎖,假設線程1拿到了鎖,執行同步代碼塊的內容,創建了實例並返回,此時線程2又獲得鎖,執行同步代碼塊內的代碼,因為此時線程1已經創建了,所以線程2雖然拿到鎖了,如果內部不加判空的話,線程2會再new一次,導致兩個線程獲得的不是同一個實例。線程安全的控制其實是內部判空在起作用,至於為什么要加外面的判空下面會說。

/**
 * 懶漢模式-線程安全,適用於多線程
 */
public class LazySafeSingleton{
    private static volatile LazySafeSingleton safeSingleton;//防止指令重排
    private LazySafeSingleton() {
    }
    public static LazySafeSingleton getInstance(){
        if(safeSingleton==null){
            synchronized (LazySafeSingleton.class){
                if(safeSingleton==null){//雙重檢測
                    safeSingleton = new LazySafeSingleton();
                }
            }

        }
        return safeSingleton;
    }
}

 當不加內層判空時,會出現不是單例的情況,只不過出現的概率更低了點。

可不可以只加內層判空呢?答案是可以。

那為什么還要加外層判空的呢?內層判空已經可以滿足線程安全了,加外層判空的目的是為了提高效率。因為可能存在這樣的情況:線程1拿到鎖后執行同步代碼塊,在new之后,還沒有釋放鎖的時候,線程2過來了,它在等待鎖(此時線程1已經創建了實例,只不過還沒釋放鎖,線程2就來了),然后線程1釋放鎖后,線程2拿到鎖,進入同步代碼塊匯總,判空,返回。這種情況線程2是不是不用去等待鎖了?所以在外層又加了一個判空就是為了防止這種情況,線程2過來后先判空,不為空就不用去等待鎖了,這樣提高了效率。

雙重檢查鎖模式是一種非常好的單例實現模式,解決了單例、性能、線程安全問題,上面的雙重檢測鎖模式看上去完美無缺,其實是存在問題,在多線程的情況下,可能會出現空指針問題,出現問題的原因是JVM在實例化對象的時候會進行優化和指令重排序操作。什么是指令重排?

上面的safeSingleton = new LazySafeSingleton();操作並不是一個原子指令,會被分割成多個指令:

1 memory = allocate(); //1:分配對象的內存空間
2 ctorInstance(memory); //2:初始化對象
3 instance = memory; //3:設置instance指向剛分配的內存地址

經過指令重排

1 memory = allocate(); //1:分配對象的內存空間
2 instance = memory; //3:設置instance指向剛分配的內存地址,此時對象還沒被初始化
3 ctorInstance(memory); //2:初始化對象

若有A線程進行完重排后的第二步,且未執行初始化對象。此時B線程來取singletonTest時,發現singletonTest不為空,於是便返回該值,但由於沒有初始化完該對象,此時返回的對象是有問題的。這也就是為什么說看似穩的一逼的代碼,實則不堪一擊。 

上述代碼的改進方法:將safeSingleton聲明為volatile類型即可(volatile有內存屏障的功能)。

private static volatile LazySafeSingleton safeSingleton;

 

2.3 內部類創建外部類實例

該方式天然線程安全,是否final根據自己需要。

 1 /**
 2  * 懶漢模式-線程安全,適用於多線程
 3  * 在內部類被加載和初始化時 才創建實例
 4  * 靜態內部類不會自動隨着外部類的加載和初始化而初始化,它是要單獨加載和初始化的。
 5  * 因為是在內部類加載和初始化時創建的 因此它是線程安全的
 6  */
 7 public class LazyInnerSingleton{
 8     private LazyInnerSingleton() {
 9     }
10     private static class Inner{
11         private static final LazyInnerSingleton INSTANCE = new LazyInnerSingleton();
12     }
13     public static LazyInnerSingleton getInstance(){
14         return Inner.INSTANCE;
15     }
16 }

 

3、破壞單例模式的方法及解決辦法

1、除枚舉方式外, 其他方法都會通過反射的方式破壞單例,反射是通過調用構造方法生成新的對象,所以如果我們想要阻止單例破壞,可以在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法如下:

1 private SingletonObject(){
2     if (instance != null){
3         throw new RuntimeException("實例已經存在,請通過 getInstance()方法獲取");
4     }
5 }

2、如果單例類實現了序列化接口Serializable, 就可以通過反序列化破壞單例,所以我們可以不實現序列化接口,如果非得實現序列化接口,可以重寫反序列化方法readResolve(), 反序列化時直接返回相關單例對象。

1   public Object readResolve() throws ObjectStreamException {
2         return instance;
3    }

4、總結

餓漢模式

  • 靜態常量 簡潔直觀容易理解
  • 枚舉 最簡潔
  • 靜態代碼塊 可以在靜態塊里做一些初始化的工作

懶漢模式

  • 單線程形式 該形式下不適用多線程,存在線程安全問題
  • 多線程形式 適用於多線程
  • 內部類形式 最簡潔

 
如果你覺得文章不錯,歡迎點贊,關注公眾號:編程大道


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM