1. 程式人生 > 程式設計 >誰要是再問你單例模式,那就拋給他這7種寫法吧!

誰要是再問你單例模式,那就拋給他這7種寫法吧!

單例設計模式是23種設計模式中,最基礎也是最常用的設計模式之一,也是面試中關於設計模式知識點考察比較高頻的問題之一。說起單例模式的寫法,大多數情況下出現在我們腦海中的可能就是“餓漢式”,“懶漢式”這兩種寫法,但是今天小碼哥今天要介紹的是單例模式的7種寫法,以後面試官要是再問你單例模式,那就拋給他這七種寫法吧!

接下來,我們就言歸正傳,來一一介紹這七種單例模式的寫法吧!

1、餓漢式

餓漢式是單例模式設計中比較經典的實現方式。實現程式碼如下:

//final不允許被繼承
public final class SingleTonEhangshi {
    //例項變數
    private byte[] data = new byte[1024];

    //在定義例項物件時直接初始化
    private static SingleTonEhangshi instance = new SingleTonEhangshi();

    //私有化建構函式,不允許外部NEW
    private SingleTonEhangshi
() { } public static SingleTonEhangshi getInstance() { return instance; } } 複製程式碼

餓漢式的實現關鍵在於instance作為類變數直接得到了初始化,如果我們主動使用SingleToEhangshi類,那麼instance例項將會直接完成建立,包括其中的例項變數也都會得到初始化。

instance作為類變數,在類初始化的過程中會被收集進()方法中,而該方法是可以100%地保證同步,也就是說instance在多執行緒的情況下不可能被初始化兩次。但是由於instance被ClassLoader載入後很長一段時間才被使用的話,那就會意味著instance例項所開闢的堆記憶體會駐留很長的時間。

總體說來,如果一個類中的成員變數比較少,且佔用的記憶體資源也不多,用餓漢式的方式實現單例模式也未嘗不可,只是其無法進行懶載入。

2、懶漢式

所謂懶漢式就是在使用類例項的時候再去建立,也就是說用到的時候我再建立,這樣就可以避免類在初始化的時候提前建立過早地佔用記憶體空間。實現程式碼如下:

//final不允許被繼承
public final class SingleTonLhangshi {
    //例項變數
    private byte[] data = new byte[1024];

    //定義例項,但是不直接初始化
    private static SingleTonLhangshi instance = null;

    //私有化建構函式,不允許外部NEW
    private SingleTonLhangshi
() { } public static SingleTonLhangshi getInstance() { if (null == instance) { instance = new SingleTonLhangshi(); } return instance; } } 複製程式碼

類變數instance=null,因此當類被初始化的時候instance並不會立刻被例項化,而是在getInstance()方法被呼叫時判斷instance例項是否被例項化,如果沒有例項化在呼叫私有構造方法進行例項化操作。

懶漢式寫法在多執行緒環境下,會存在同一時間多個執行緒同時看到null==instance的情況,從而導致instance會被例項化多次,從而無法保證單例的唯一性。

3、懶漢式+同步方法

懶漢式的單例實現方式可以保證例項的懶載入,但是卻無法保證例項的唯一性。在多執行緒環境下由於instance為共享資料,當多個執行緒訪問使用時,需要保證資料的同步性,所以如果需要保證懶漢式例項的唯一性,我們可以通過同步的方式來實現。程式碼如下:

/final不允許被繼承
public final class SingleTonLhangshiSync {
    //例項變數
    private byte[] data = new byte[1024];

    //定義例項,但是不直接初始化
    private static SingleTonLhangshiSync instance = null;

    //私有化建構函式,不允許外部NEW
    private SingleTonLhangshiSync() {

    }

    //向getInstance方法加入同步控制,每次只能有一個執行緒能夠進入
    public static synchronized SingleTonLhangshiSync getInstance() {
        if (null == instance) {
            instance = new SingleTonLhangshiSync();
        }
        return instance;
    }
}
複製程式碼

採用懶漢式+資料同步的方法既滿足了懶載入又能夠100%保證instance例項的唯一性。但是,synchronized關鍵字的排它性會導致getInstance()方法同一時刻只能被一個執行緒訪問,效能會比較低下。

4、Double-Check

Double-Check是一種比較聰明的設計方式,它提供了一種高效的資料同步策略,那就是首次初始化的時候加鎖,之後則允許多個執行緒同時進行getInstance()方法的呼叫來獲得類的例項。程式碼如下:

//final不允許被繼承
public final class SingletonDoubleCheck {
    //例項變數
    private byte[] data = new byte[1024];

    //定義例項,但是不直接初始化
    private static SingletonDoubleCheck instance = null;

    Connection con;
    Socket socket;

    //私有化建構函式,不允許外部NEW
    private SingletonDoubleCheck() {
        this.con = con;//初始化
        this.socket = socket;//初始化

    }
    public static SingletonDoubleCheck getInstance() {
        //當instance為null時,進入同步程式碼塊,同時該判斷避免了每次都需要進入同步程式碼塊,可以提高效率
        if (null == instance) {
            //只有一個執行緒能夠獲得SingletonDoubleCheck.class關聯的monitor
            synchronized (SingletonDoubleCheck.class) {
                //判斷如果instance為null則建立
                if (null == instance) {
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}
複製程式碼

當兩個執行緒發現null==instance成立時,只有一個執行緒有資格進入同步程式碼塊,完成對instance的初始化,隨後的執行緒發現null==instance不成立則無須進行任何操作,以後對getInstance的訪問就不會再需要進行資料同步了。

此種方式看起來是既滿足了懶載入,又保證了instance例項的唯一性,並且還提供了比較高效的資料同步策略,可以允許多個執行緒同時對getInstance進行訪問。但是這種方式在多執行緒的情況下,可能會引起空指標異常,這是因為如果在如上程式碼的構造方法中還存在初始化其他資源的情況的話,由於JVM執行時存在指令重排的情況,這些資源在例項化時順序並無前後關係的約束,那麼在這種情況下,就極有可能是instance最先被例項化,而con和socket並未完成例項化,而未完成例項化的例項在呼叫其方法時將會丟擲空指標異常。

5、Volatile+Double-Check

為瞭解決Double-Check指令重排導致的空指標問題,可以用volatile關鍵字防止這種重排序的發生。因此程式碼只需要稍作修改就能滿足多執行緒下的單例、懶載入以及例項的高效性了。程式碼如下:

//final不允許被繼承
public final class SingletonDoubleCheck {
    //例項變數
    private byte[] data = new byte[1024];

    //定義例項,但是不直接初始化
    private static volatile SingletonDoubleCheck instance = null;

    Connection con;
    Socket socket;

    //私有化建構函式,不允許外部NEW
    private SingletonDoubleCheck() {
        this.con = con;//初始化
        this.socket = socket;//初始化

    }

    public static SingletonDoubleCheck getInstance() {
        //當instance為null時,進入同步程式碼塊,同時該判斷避免了每次都需要進入同步程式碼塊,可以提高效率
        if (null == instance) {
            //只有一個執行緒能夠獲得SingletonDoubleCheck.class關聯的monitor
            synchronized (SingletonDoubleCheck.class) {
                //判斷如果instance為null則建立
                if (null == instance) {
                    instance = new SingletonDoubleCheck();
                }
            }
        }
        return instance;
    }
}
複製程式碼

6、Holder方式

Holder方式完全藉助了類載入的特點。程式碼如下:

//不允許被繼承
public final class SingletonHolder {
    //例項變數
    private byte[] data = new byte[1024];

    private SingletonHolder() {

    }

    //在靜態內部類中持有單例類的例項,並且可直接被初始化
    private static class Holder {
        private static SingletonHolder instance = new SingletonHolder();
    }

    //呼叫getInstance方法,事實上是獲得Holder的instance靜態屬性
    public static SingletonHolder getInstance() {
        return Holder.instance;
    }
}
複製程式碼

在單例類中並沒有instance的靜態成員,而是將其放到了靜態內部類Holder之中,因此單例類在初始化的過程中並不會建立SingletonHolder的例項,內部類Holder中定義了SingletonHolder的靜態變數,並且直接進行了例項化,只有當Holder被主動引用的時候才會建立SingletonHolder的例項。

SingletonHolder例項的建立過程在Java程式編譯時期收集至()方法中,該方法又是同步方法,可以保證記憶體的可見性、JVM指令的順序性和原子性。Holder方式的單例模式設計是最好的設計之一,也是目前使用比較廣的設計。

7、列舉方式

列舉方式在很多開源框架中也應用得比較廣泛,列舉型別不允許被繼承,同樣是執行緒安全的,並且只能被例項化一次,但是列舉型別不能夠實現懶載入。用列舉型別,實現單例模式的程式碼如下:

public class SingletonEnum {
    //例項變數
    private byte[] data = new byte[1024];

    private SingletonEnum() {

    }

    //使用列舉充當Holder
    private enum EnumHolder {
        INSTANCE;
        private SingletonEnum instance;

        EnumHolder() {
            this.instance = new SingletonEnum();
        }

        private SingletonEnum getInstance() {
            return instance;
        }
    }

    public static SingletonEnum getInstance() {
        return EnumHolder.INSTANCE.getInstance();
    }
}
複製程式碼

以上就是要給大家介紹的單例模式的7種寫法了,雖然單例模式非常簡單,但是在多執行緒的情況下,我們之前所設計的單例程式未必能夠滿足單例項、懶載入以及高效能的特點。