您的位置:首頁>正文

跟小編一起學:單例這種設計模式

隨著我們編寫代碼的深入, 我們或多或少都會接觸到設計模式, 其中單例(Singleton)模式應該是我們耳熟能詳的一種模式。 本文將比較特別的介紹一下Java設計模式中的單例模式。

概念

單例模式, 又稱單件模式或者單子模式, 指的是一個類只有一個實例, 並且提供一個全域訪問點。

實現思路

在單例的類中設置一個private靜態變數sInstance, sInstance類型為當前類, 用來持有單例唯一的實例。

將(無參數)構造器設置為private, 避免外部使用new構造多個實例。

提供一個public的靜態方法, 如getInstance, 用來返回該類的唯一實例sInstance。

其中上面的單例的實例可以有以下幾種創建形式,

每一種實現都需要保證實例的唯一性。

餓漢式

餓漢式指的是單例的實例在類裝載時進行創建。 如果單例類的構造方法中沒有包含過多的操作處理, 餓漢式其實是可以接受的。

餓漢式的常見代碼如下,當SingleInstance類載入時會執行private static SingleInstance sInstance = new SingleInstance();初始化了唯一的實例, 然後getInstance()直接返回sInstance即可。

public class SingleInstance {

private static SingleInstance sInstance = new SingleInstance();

private SingleInstance() {

}

public static SingleInstance getInstance() {

return sInstance;

}

}

餓漢式的問題

如果構造方法中存在過多的處理, 會導致載入這個類時比較慢, 可能引起性能問題。

如果使用餓漢式的話, 只進行了類的裝載, 並沒有實質的調用, 會造成資源的浪費。

懶漢式

懶漢式指的是單例實例在第一次使用時進行創建。 這種情況下避免了上面餓漢式可能遇到的問題。

但是考慮到多執行緒的併發操作, 我們不能簡簡單單得像下面代碼實現。

public class SingleInstance {

private static SingleInstance sInstance;

private SingleInstance() {

}

public static SingleInstance getInstance() {

if (null == sInstance) {

sInstance = new SingleInstance();

}

return sInstance;

}

}

上述的代碼在多個執行緒密集調用getInstance時, 存在創建多個實例的可能。 比如執行緒A進入null == sInstance這段代碼塊, 而在A執行緒未創建完成實例時, 如果執行緒B也進入了該代碼塊, 必然會造成兩個實例的產生。

synchronized修飾方法

使用synchrnozed修飾getInstance方法可能是最簡單的一個保證多執行緒保證單例唯一性的方法。

synchronized修飾的方法後, 當某個執行緒進入調用這個方法, 該執行緒只有當其他執行緒離開當前方法後才會進入該方法。 所以可以保證getInstance在任何時候只有一個執行緒進入。

public class SingleInstance {

private static SingleInstance sInstance;

private SingleInstance() {

}

public static synchronized SingleInstance getInstance() {

if (null == sInstance) {

sInstance = new SingleInstance();

}

return sInstance;

}

}

但是使用synchronized修飾getInstance方法後必然會導致性能下降, 而且getInstance是一個被頻繁調用的方法。 雖然這種方法能解決問題, 但是不推薦。

雙重檢查加鎖

使用雙重檢查加鎖,

首先進入該方法時進行null == sInstance檢查, 如果第一次檢查通過, 即沒有實例創建, 則進入synchronized控制的同步塊,並再次檢查實例是否創建, 如果仍未創建, 則創建該實例。

雙重檢查加鎖保證了多執行緒下只創建一個實例, 並且加鎖代碼塊只在實例創建的之前進行同步。 如果實例已經創建後, 進入該方法, 則不會執行到同步塊的代碼。

public class SingleInstance {

private static volatile SingleInstance sInstance;

private SingleInstance() {

}

public static SingleInstance getInstance() {

if (null == sInstance) {

synchronized (SingleInstance.class) {

if (null == sInstance) {

sInstance = new SingleInstance();

}

}

}

return sInstance;

}

}

volatile是什麼

Volatile是羽量級的synchronized, 它在多處理器開發中保證了共用變數的“可見性”。 可見性的意思是當一個執行緒修改一個共用變數時, 另外一個執行緒能讀到這個修改的值。 使用volatile修飾sInstance變數之後, 可以確保多個執行緒之間正確處理sInstance變數。

關於volatile, 可以訪問深入分析Volatile的實現原理瞭解更多。

利用static機制

在Java中, 類的靜態初始化會在類被載入時觸發,

我們利用這個原理, 可以實現利用這一特性, 結合內部類, 可以實現如下的代碼, 進行懶漢式創建實例。

public class SingleInstance {

private SingleInstance() {

}

public static SingleInstance getInstance() {

return SingleInstanceHolder.sInstance;

}

private static class SingleInstanceHolder {

private static SingleInstance sInstance = new SingleInstance();

}

}

關於這種機制, 可以具體瞭解雙重檢查鎖定與延遲初始化

好奇問題

真的只有一個物件麼

其實, 單例模式並不能保證實例的唯一性, 只要我們想辦法的話, 還是可以打破這種唯一性的。 以下幾種方法都能實現。

使用反射, 雖然構造器為非公開, 但是在反射面前就不起作用了。

如果單例的類實現了cloneable, 那麼還是可以拷貝出多個實例的。

Java中的物件序列化也有可能導致創建多個實例。 避免使用readObject方法。

使用多個類載入器載入單例類, 也會導致創建多個實例並存的問題。

單例可以繼承麼

單例類能否被繼承需要分情況而定。

可以繼承的情況

當子類是父類單例類的內部類時, 繼承是可以的。

public class BaseSingleton {

private static volatile BaseSingleton sInstance;

private BaseSingleton() {

}

public static BaseSingleton getInstance() {

if (null == sInstance) {

synchronized(BaseSingleton.class) {

if (null == sInstance) {

sInstance = new BaseSingleton();

}

}

}

return sInstance;

}

public static class MySingleton extends BaseSingleton {

}

}

但是上面僅僅是編譯和執行上允許的, 但是繼承單例沒有實際的意義, 反而會變得更加事倍功半, 其代價要大於新寫一個單例類。 感興趣的童鞋可以嘗試折騰一下。

不可以繼承的情況

如果子類為單獨的類, 非單例類的內部類的話, 那麼在編譯時就會出錯Implicit super constructor BaseSingleton() is not visible for default constructor. Must define an explicit constructor, 主要原因是單例類的構造器是private, 解決方法是講構造器設置為可見, 但是這樣做就無法保證單例的唯一性。 所以這種方式不可以繼承。

總的來說, 單例類不要繼承。

單例 vs static變數

全域靜態變數也可以實現單例的效果, 但是使用全域變數無法保證只創建一個實例, 而且使用全域變數的形式, 需要團隊的約束,執行起來可能會出現問題。

關於GC

因為單例類中又一個靜態的變數持有單例的實例,所以相比普通的物件,單例的物件更不容易被GC回收掉。單例物件的回收應該發生在其類載入器被GC回收掉之後,一般不容易出現。

需要團隊的約束,執行起來可能會出現問題。

關於GC

因為單例類中又一個靜態的變數持有單例的實例,所以相比普通的物件,單例的物件更不容易被GC回收掉。單例物件的回收應該發生在其類載入器被GC回收掉之後,一般不容易出現。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示