Java設計模式——單例模式(Singleton pattern)

JeromeLiee發表於2018-01-24

眾所周知,在程式碼中採用合理的設計模式,不僅僅能使程式碼更容易被他人理解,同時也能使整體模組擁有更合理的結構,方便後期擴充套件維護。因此就產生了一些“套路”,而這些“套路”我們便稱之為“設計模式”。

另外,如果想要弄明白一些知識,一定要分清楚順序,即 遇到了什麼問題要怎麼解決 以及 有沒有更好的辦法,這樣帶著問題去思考,可以達到事半功倍的效果。

言歸正傳,開始說單例模式。按照上面的思考順序,我們一步一步來分析。

1. 有本參奏,無本退朝

開始上早朝了啊~平常我們在使用某個類的例項時,直接使用關鍵字new,便可建立一個例項物件。但有時候可能會頻繁使用某個例項物件,或者建立這個物件比較耗費資源,例如請了一個管家,需要管家幫你幹一些事,總不能每次需要管家的時候就重新聘請一個吧?最好的方法就是長期聘請這個管家,需要的時候直接吩咐就行了。突然發現我這個例子舉得是很恰當啊!

通過上面的闡述,我們遇到一個問題,那就是某個類的例項物件頻繁使用,或者建立時比較費時費事時,希望只建立一次物件,並且一個就夠了(你要是非得請兩個管家,我只能說你有錢)。在這種情況下,我們來開始思考如果解決這個問題。

2. 建言獻策,百花齊放

大家都開始獻上良策啊,一個一個來,第一位,趙學士你先發言~

2.1 餓漢式

其實挺好解決的,看我下面的程式碼:

// 趙學士的方案
public class Singleton {
    private static Singleton sInstance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return sInstance;
    }
}
複製程式碼

構造方法私有化這就不解釋了,保證外部不能隨便通過new關鍵字來建立物件;靜態成員變數sIntanceSingleton這個類載入的時候就初始化,建立了Singleton物件,並且只存在一個;通過Singleton.getInstance()方法可以獲取該例項物件,這就是單例模式!這就解決了問題啊同志們!

不過錢大臣想了想說,不過這種方法好像有點弊端,假如我現在還不需要管家,總不能讓我白花錢養著吧?能不能在我需要的時候再花錢聘請管家?

誒~~你這麼一說也有道理啊,那錢大臣,說說你的辦法。

2.2 懶漢式

話不多說,先看程式碼:

// 錢大臣的方案
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}
複製程式碼

怎麼樣?這個辦法不錯吧!成員變數預設初始化不建立物件,當呼叫Singleton.getInstance()方法時,如果sInstancenull再建立物件,否則就直接返回,保證了你的要求。

此時孫丞相“哼”了一下說,你這還不如趙學士呢!趙學士有可能提前白花錢聘請了一個管家,而你有可能多花錢請了好幾個管家呢!你都沒有考慮到多執行緒的情況!錢大臣一聽趕緊做了修改,程式碼如下:

// 錢大臣的方案2
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public synchronized static Singleton getInstance() {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}
複製程式碼

給getInstance方法加了關鍵字synchronized,保證建立物件的時候只有一個呼叫者,可以了吧?孫丞相又“哼”了一聲,可每次呼叫的時候都會因為這個鎖帶來的時間開銷,你以為開鎖不要時間啊?效能低下!錢大臣臉有點紅,於是又做了修改:

// 錢大臣的方案3
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (sInstance == null) {
            synchronized (Singleton.class) {
                sInstance = new Singleton();
            }
        }
        return sInstance;
    }
}
複製程式碼

sInstance = new Singleton();語句加了鎖,應該沒問題了吧?孫丞相第三次“哼”了一聲,我給你假設個情況啊,設現有執行緒A和B,在某個時刻兩個執行緒都通過了判空語句但都沒有取到鎖資源,然後執行緒A先取得鎖資源進入臨界區(被鎖的程式碼塊),建立了一個物件,然後退出臨界區,釋放鎖資源。接著執行緒B取得鎖資源進入臨界區,開始建立物件,退出臨界區,釋放鎖資源,請問現在有幾個Sinleton物件? 錢大臣聽後說那我直接把鎖加到判空語句之前!

// 錢大臣的方案4
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (sInstance == null) {
                sInstance = new Singleton();
            }
        }
        return sInstance;
    }
}
複製程式碼

孫丞相直接笑了,說你這樣和在方法上加synchronized關鍵字有什麼區別。連續被懟,錢大臣感覺很沒面子直接反駁道,you can you up, no can no bb!

2.3 雙重校驗鎖DCL(double checked locking)

孫丞相大手一揮說道,看好了啊,今兒讓我教教你怎麼做人!

// 孫丞相的方案
public class Singleton {
    private static volatile Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (sInstance == null) {
            synchronized (Singleton.class) {
                if (sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}
複製程式碼

首先,方法鎖改成程式碼塊鎖,減少鎖的範圍;其次第一次判空,在單執行緒的情況下提升了效率,但此時如果同時存在兩個執行緒併發情況,即都判空成功,接下來會由鎖內的第二次判空來過濾。還是剛才的例子,假設現有執行緒A和B,在某個時刻兩個執行緒都通過了第一次判空語句但都沒有取到鎖資源。然後執行緒A先取得鎖資源進入臨界區(被鎖的程式碼塊),執行第二次判空語句,判空成功,建立了一個物件,然後退出臨界區,釋放鎖資源。接著執行緒B取得鎖資源進入臨界區,執行判空語句發現不通過,直接退出臨界區,釋放鎖資源。

另外在成員變數sInstance前面加了一個volatile關鍵字,這個特別重要。容我裝個逼:在Java記憶體模型(JMM)中,並不限制處理器的指令順序,說白了就是在不影響結果的情況下,順序可能會被打亂。

在執行sInstance = new Singleton();這條命令語句時,JMM並不是一下就執行完畢的,即不是原子性,實質上這句命令分為三大部分:

  1. 為物件分配記憶體
  2. 執行構造方法語句,初始化例項物件
  3. 把sInstance的引用指向分配的記憶體空間

在JMM中這三個步驟中的2和3不一定是順序執行的,如果執行緒A執行的順序為1、3、2,在第2步執行完畢的時候,恰好執行緒B執行第一次判空語句,則會直接返回sInstance,那麼此時獲取到的sInstance僅僅只是不為null,實質上沒有初始化,這樣的物件肯定是有問題的!

volatile關鍵字的存在意義就是保證了執行命令不會被重排序,也就避免了這種異常情況的發生,所以這種獲取單例的方法才是真正的安全可靠!

一直默默不做聲的李將軍冷不丁地開口了,孫丞相啊,你不覺得你這樣寫很麻煩嗎?我有更簡單的寫法呢!

2.4 靜態內部類實現的單例模式

你看你這又是判空又是加鎖的,多麻煩,其實可以通過靜態內部類的方式,既保證了只存在一個單例,又保證了執行緒安全,程式碼如下:

// 李將軍的方案
public class Singleton {

    private Singleton() {
    }

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

    private static class SingletonHolder {
        private static Singleton sInstance = new Singleton();
    }
}
複製程式碼

當外部類Singleton被載入時,其靜態內部類SingeletonHolder不會被載入,所以它的成員變數sInstance是不會被初始化的,只有當呼叫Singleton.getInstance()方法時,才會載入SingeletonHolder並且初始化其成員變數,而類載入時是執行緒安全的,這樣既保證了延遲載入,也保證了執行緒安全,同時也簡化了程式碼量,一舉三得!

2.5 列舉單例

在說完上面4種單例模式的實現方式之後,不知道大家有沒有想到過一個問題,那就是序列化。我們可以通過以下程式碼將例項寫入磁碟,然後再從磁碟讀出,即使構造方法是私有的,反序列化也是可以通過特殊的途徑去重新建立一個新的例項,程式碼如下:

public Singleton createNewInstance() throws IOException, ClassNotFoundException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    // 此處的singleton為通過單例模式獲取到的例項物件
    oos.writeObject(singleton);

    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    // 此時返回一個反序列化後得到的新的例項物件
    return (Singleton) ois.readObject();
}
複製程式碼

可以通過上面的程式碼看到,反序列化後可以得到一個新的例項物件,那麼這種現象沒法避免了嗎?其實是可以避免的。反序列化提供了一個很特別的方法,即一個私有、被例項化的方法readResolve(),這個方法可以讓開發人員控制物件的反序列化。想要杜絕上面現象的發生,那麼就可以在單例模式中加入readResolve()方法,程式碼如下:

private Object readResolve() {
    // 此處返回單例模式中的例項物件
    return sInstance;
}
複製程式碼

在《Effective Java》一書中,作者Joshua Bloch提倡可以採用列舉的方式來解決上述出現的所有問題,程式碼如下:

// 外國老大哥Joshua Bloch的方案
public enum SingletonEnum {
    INSTANCE;
    
    public void method(){
        // do something...
    }
}

複製程式碼

可以通過SingletonEnum.INSTANCE獲取單例,然後再呼叫內部的各種方法。列舉實現單例有如下好處:

  1. 例項的建立執行緒安全,確保單例;
  2. 防止被反射建立多個例項;
  3. 沒有序列化的問題。

雖然這種方法還沒有被廣泛採用,但是單元素的列舉型別已經成為實現Singleton單例模式的最佳方法。

3. 總結

通過上面的一步步分析,不知道大家有沒有對單例模式有個新的認識呢?總的來說,加了volatile關鍵字的雙重校驗鎖和靜態內部類實現的單例模式是目前應用最為廣泛的,如果你們要求更嚴的話,那麼列舉單例也不失為一個獲取單例更加的方式。歡迎各位能多多交流,指出不足,共同學習進步!

相關文章