Java 設計模式之單例模式詳解

本文學習一個Java單例模式。

單例模式

單例,顧名思義,就是隻存在一個實例。或許,你也會疑問?爲什麼會使用到單例模式呢?這是因爲,很多情況下,我們需要一個實例,比如線程池、緩存、驅動等,如果存在多個實例,那將會導致混亂。

首先我們複習下構造函數,

構造函數

構造函數的用處是實例化對象,
我們的用法通常是這樣,

public class ClassA {
    public ClassA() {
    }
}

用到該類的時候,我們可以這樣實例化,

ClassA a = new ClassA();

有兩個地方需要我們注意,
1. 構造函數名與類名一致
2. 權限修飾符爲public
關於上述兩條,第1條,類名一致是Java規定的,那有沒有想過第2條,權限修飾符必須是public麼?答案是不是,我們可以將其修改爲private,但是這樣就會帶來另一個問題,由於是private,在其他類中,無法調用該函數,所以就無法實例化,那麼如何實例化呢?

既然構造函數無法訪問,那麼我們能不能通過其他函數來得到該類的實例呢?答案是可以的。

public class ClassA {
    private ClassA() {
    }

    public static ClassA getClassA(){
        return new ClassA();
    }
}

上面代碼中,提供了一個getClassA方法,該方法的返回類型是ClassA,函數的主體是返回一個ClassA的實例,注意到該函數是靜態函數(static),爲什麼是靜態函數呢?靜態函數與非靜態函數的最主要區別在於,靜態函數可以直接通過類名來調用,而非靜態函數必須通過對象實例化來引用。
然後,我們可以這樣來獲取ClassA實例,

ClassA a = ClassA.getClassA();

那麼,我們將其修改成下面代碼,就成爲了單例模式,

單例1

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {
    }

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

首先聲明一個實例uniqueInstance,在getInstance方法中,判斷uniqueInstance是否爲null,若爲null,則實例化並賦值,若不爲null,則直接返回。該方法保證了uniqueInstance爲唯一的實例。
然而,在多線程下,該方法卻會出現問題,因爲,如果線程1和線程2同時調用該函數,將會出現同時修改現象。在操作系統中,這屬於多線程同步問題,我們可以將其獲取實例方法定義爲同步方法,就可以避免該現象。

單例2

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

synchronized 關鍵字的意思是,同步。意味着,在同一時刻,只能有一個對象調用該方法,其他調用方法處於等待狀態,直到調用結束,其他調用方法纔會一一執行(同步)。這樣就保證了不會同時修改對象。
然而,該方法也存在一個缺點,那就是如果很多個地方都需要調用getInstance方法的話,那樣JVM的壓力很大,因爲它要保證這麼多個調用都是同步調用(一個一個順序執行),同時,也會導致多個線程在getInstance方法上等待,這樣顯然帶來了較大的性能影響。我們可以使用方法3和方法4來改進.

單例3

我們將代碼修改成下面這樣,

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {
    }

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

和單例2最主要的區別,在於uniqueInstance唯一實例,是在加載類的時候創建的(static關鍵字造成的),這樣就可以保證,在所有調用getInstance之前,uniqueInstance已被初始化,該方法也不會給JVM帶來額外的負擔。

單例4

我們將代碼修改爲,

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;
    }
}

與單例2的區別在於,synchronized 關鍵字不是作用在getInstance方法上,這樣調用getInstance方法並不會給JVM帶來負擔(JVM並不需要對線程調用進行調整,進行順序執行)。
當多個線程調用getInstance方法時,該方法並不會阻塞,而是會在代碼塊上進行阻塞。在同步代碼塊內,爲什麼要再次檢查是否爲空呢?設想以下情形,10個線程在實例爲null的情況下同時調用getInstance方法,那麼只會有一個線程(姑且叫做線程A)能得到Singleton的鎖,其他線程都會在同步代碼塊上阻塞,當線程A執行完畢後,此時uniqueInstance應該不爲null,但是也不能確保其一定不是null,如果線程A沒有正常執行完畢,就被中斷了呢?所以我們在同步代碼塊裏需要再次判斷一下uniqueInstance 是否爲null,若不爲null,直接返回就是,若爲null,則要重新實例化。

好了,就寫到這裏,希望您能有所收穫,有啥問題,可以私信或回覆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章