天天看點

深入了解單例模式

前言

有一些對象其實我們隻需要一個,比方說:線程池,緩存,對話框,處理偏好設定和系統資料庫的對象,日志對象,充當列印機,顯示卡等裝置的驅動程式的對象。事實上,這類對象隻能有一個執行個體,如果制造出多個執行個體,就會導緻許多問題産生,例如:程式的行為異常,資源使用過量,或者是不一緻的結果

涉及到一些類加載的知識,如果不清楚,可以看一下這篇分享:Java類的加載順序

單例模式確定一個類隻有一個執行個體,并提供一個全局通路點,實作單例模式的方法是私有化構造函數,通過getInstance()方法執行個體化對象,并傳回這個執行個體

實作

第一種(懶漢)

按照上面的想法,我們有了第一個實作

// code1
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}           

複制

當2個線程同時進入getInstance()的if語句裡面,會傳回2個不同執行個體,是以這種方式是線程不安全的

// code2
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}           

複制

用synchronized修飾可以保證線程安全,但是隻有第一次執行此方法時才需要同步,設定好 uniqueInstance,就不需要同步這個方法了,之後每次調用這個方法,同步都是一種累贅

第二種(雙重檢查鎖定)

synchronized鎖的粒度太大,人們就想到通過雙重檢查鎖定來降低同步的開銷,下面是執行個體代碼

// code3
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}           

複制

如上面代碼所示,如果第一次檢查uniqueInstance不為null,那麼就不需要執行下面的加鎖和初始化操作,可以大幅降低synchronized帶來的性能開銷,隻有在多個線程試圖在同一時間建立對象時,會通過加鎖來保證隻有一個線程能建立對象

雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優化!線上程執行到getInstance()方法的第4行,代碼讀取到uniqueInstance不為null時,uniqueInstance引用的對象有可能還沒有完成初始化

簡單概述一下《Java并發程式設計的藝術》的解釋,

uniqueInstance = new Singleton()可以分解為如下三行僞代碼

memory = allocate();    // 1:配置設定對象的記憶體空間
ctorInstance(memory);   // 2:初始化對象
uniqueInstance = memory;// 3:設定uniqueInstance指向剛配置設定的記憶體位址           

複制

3行僞代碼中的2和3之間,可能會被重排序,重排序後執行時序如下

memory = allocate();    // 1:配置設定對象的記憶體空間
uniqueInstance = memory;// 3:設定uniqueInstance指向剛配置設定的記憶體位址
                        // 注意,此時對象還沒有被初始化
ctorInstance(memory);   // 2:初始化對象           

複制

多個線程通路時可能出現如下情況

時間

線程A

線程B

t1

A1:配置設定對象的記憶體空間

t2

A3:設定uniqueinstance指向記憶體空間

t3

B1:判斷uniqueinstance是否為空

t4

B2:由于uniqueinstace不為null,線程B間通路uniqueinstance引用的對象

t5

A2:初始化對象

t6

A4:通路instace引用的對象

這樣會導緻線程B通路到一個還未初始化的對象,此時可以用volatile來修飾Singleton,這樣3行僞代碼中的2和3之間的重排序,在多線程環境中将會被禁止

// code4
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}           

複制

第三種(餓漢)

如果應用程式總是建立并使用單例式例,或者在建立和運作時方面的負擔不太繁重,我們可以以餓漢式的方式來建立單例

// code5
public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }
}           

複制

// code6
public class Singleton {

    private static Singleton uniqueInstance;

    static {
        uniqueInstance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }
}           

複制

在類加載的時候直接建立這個對象,這樣既能提高效率,又能保證線程安全,code5和code6幾乎沒有差別,因為靜态成員變量和靜态代碼塊都是類初始化的時候被加載

第四種(靜态内部類)

// code7
public class Singleton {

    private static class SingletonHolder {
        private static Singleton uniqueInstance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonHolder.uniqueInstance;
    }
}           

複制

餓漢式的方式隻要Singleton類被裝載了,那麼uniqueInstance就會被執行個體化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,uniqueInstance不一定被初始化。因為SingletonHolder類沒有被主動使用,隻有顯示通過調用getInstance方法時,才會顯示裝載SingletonHolder類,進而執行個體化uniqueInstance

第五種(枚舉)

// code8
public enum Singleton {  
   INSTANCE;  
   public void whateverMethod() {  
   }  
}           

複制

公認的實作單例的最好方式,網上資料也比較少,還沒有徹底了解清楚,說不定以後會補一篇文章說明,本篇文章中的code1和code3不建議使用,原因已經說明

參考書籍

《Java并發程式設計的藝術》

《Head First設計模式》