設計模式之單例模式(自用)

單例模式,顧名思義,指的是一個類只存在一個實例。

那么,如何保證某一個類只存在一個實例呢?對象的創建是通過類的構造函數來實現的(也可以通過clone方式,但是這種方法前提是首先有一個對象;或者通過反序列化,這種方法同樣也需要原本類的對象存在),所以就需要保證類的構造函數只能被調用一次。若是將構造函數的權限設置為public的,這樣顯然無法滿足要求。所以需要將構造方法隱藏起來,而且構造方法只在類還未創建實例的情況下被調用,如果類已經創建了實例,則不再調用。

所以可以將構造函數的訪問權限設置為最私密的private,同時暴露一個公共的接口給調用者,而在接口處統一的返回實例給調用者。如此,對于每一個需要對象的調用者,可以通過這個接口保證返回的都是同一個實例。

這樣的話,類就需要持有一個自身類的實例,當需要調用者調用公共的接口想要獲得這個類的實例的時候,統一的將這個實例返回。

public class Singleton{

private static ?Singleton Instance=new Singleton();//持有一個類的實例

private Singleton(){};//私有的構造方法

public static Singleton getInstance(){

????return Singleton.instance;?

????}

}

這種方式實現的單例模式也叫做餓漢式,實例的創建時機是在類加載的時候靜態變量初始化的時候。至于為什么叫做餓漢式,可能是因為這種方式是在類加載的時候獲得唯一實例的,而不考慮類加載時是否需要這個類的實例(可能只是初次調用這個類的其他類方法就會進行唯一的創建)。

與這種方法不同的另一種實現叫做懶漢式,可能是因為懶漢是要到事情迫在眉睫的才會去做,而懶漢式的單例模式實現這種方式是在需要這個類的實例再去創建這個實例,是一種延遲創建對象的實現方式。

那么,如何實現這種呢?即不是讓類加載的時候就進行對象創建,而是等到真正需要這個對象的時候(也就是調用公共的接口想要獲得實例的時候),可以想到的是,將創建對象的過程放到接口內,這樣就可以保證創建對象的時機是在調用這個公共接口的時候。但是顯然僅僅這樣做,是沒有辦法保證實例是唯一的,每一次的調用都將會創建一個新的實例返回。

可以想到的一種辦法,在每次調用公共接口的時候,檢查是否這個類的實例是否已經創建,這樣就需要一個標識,來標識類的實例是否已經被創建,如果被創建了,就直接返回這個類的實例,如果沒有創建,就先創建,再返回。所以這種方式,類還是需要持有自身的實例,由于類的實例持有是通過保存這個對象的引用來實現的,所以可以通過判斷這個引用是否為null,就可以判斷實例是否已經被創建。

public class Singleton{

private static ?Singleton Instance=null;

private Singleton(){};//私有的構造方法

public static Singleton getInstance(){

????if(instance==null)

????instance=new Singleton();

????return Singleton.instance;

????}

}

這種就是懶漢式的設計單例模式了,這種方式雖然是實現了延遲創建對象的功能(由于對象的創建是需要相當程度的占用資源,如果大量的對象創建都集中在類加載時,這樣是非常不好的),但是這種方式只能保證在單線程下是安全的,也就是在多線程下是有問題的。

原因是因為,由于對象的創建不具有原子性,對象的創建可以簡單的分為幾個階段1、在堆中開辟可用的空間 2、對象進行初始化 3、將對象的引用復制給引用變量,也就是“=”這個操作。java虛擬機jvm會對指令進行重排序來優化程序,jvm會保證單線程下重排序不會對結果產生影響,假設重排序可能是先把對象的引用傳給引用變量,然后再進行初始化對象,就算是這樣,只要保證在調用對象之前,已經完成了初始化,那么重排序將不會影響結果。但是在多線程的情況之下,這種重排序的機制將可能出現問題,假設線程1為第一個調用new Singleton的線程,而且在它創建對象的時候發生了重排序,jvm在未將對象初始化的引用先傳遞給了引用變量instance,而此時,線程2也在訪問getInstance(),那么當它進行instance==null判斷的時候,將會認為對象已經創建(實際上對象還未初始化)而直接返回,如果此時再繼續進行對象的調用,將會發生錯誤,因為對象沒有初始化完成。而且多個線程最大權限訪問同一個資源,也是會出問題的。(線程1進行對象初始化后還未進行引用賦值,線程2此時將判斷對象還未創建,從而進入if子句中進行對象創建,這樣就無法保證實例唯一)

那么如何解決這個問題呢?首先想到的可能是加鎖,既然在對象創建的未完成的時候可能有其他的線程訪問這個資源,那么可以將這個getInstance()加上synchronized關鍵字,每次只允許一個線程訪問這個方法,這樣確實杜絕了上面所說的問題,但是由于在這么大的粒度下面加上同步,將會非常影響效率。

public class Singleton{

private static ?Singleton Instance=null;

private Singleton(){};//私有的構造方法

public static synchronized Singleton getInstance(){

????if(instance==null)

????instance=new Singleton();

????return Singleton.instance;

}

}

因為這種方法效率非常差,所以還需要另外想辦法解決多線程下單例模式的問題。所以有人想到一種將同步的粒度縮小的辦法,叫做雙重校驗鎖。

public class Singleton{

private static ?Singleton Instance=null;

private Singleton(){};//私有的構造方法

public static Singleton getInstance(){

if(instance==null){

????synchronized(Singleton.class){

????????if(instance==null){

????????????instance=new Singleton();

????????}

????}

}

return Singleton.instance;

}

}

這種方法解決了上一個方法效率的問題,也解決了只創建唯一的實例,但是由于重排序機制,將可能會導致雙重校驗鎖失效,想到了既然問題是重排序機制下其他線程訪問了未初始化完成的對象造成的可能錯誤。那么可以想辦法讓jvm不進行重排序,volatile關鍵字有兩種語義,一個是線程間的可見性,保證其標識的值是最新的值,從內存中讀取,而不是緩存的值,另一種語義是禁止重排序。所以可以用volatile解決雙重校驗鎖失效的問題。?

public class Singleton{

private ?volatile static ?Singleton Instance=null;

private Singleton(){};//私有的構造方法

public static Singleton getInstance(){

if(instance==null){

synchronized(Singleton.class){

if(instance==null){

instance=new Singleton();

}

}

}

return Singleton.instance;

}

}

還有一種方法是靜態內部類的方法,餓漢式的方式沒有多線程下的諸多問題,但是由于其不具有延遲性,而不被采用,但是可以將這種類加載時由類直接創建實例的方式的優點利用,通過靜態內部類的方式,這樣既保證了延遲創建對象,又多線程安全。

public class Singleton{

? ? private static class SingletonHolder{

? ? ? ? private static Singleton instance=new Singleton();

????}

private Singleton(){};

public static Singleton getInstance(){

? ? ? ? return SingletonHoder.instance;

????}

}

上面的所有的類內的單例對象引用也可以定義為final。

還有一種是枚舉的方式,由于枚舉類型的枚舉值的對象創建是由java自己實現的,它的構造器是私有的。

public enum Singleton{

INSTANCE;//枚舉值,java會在創建class的時候將它定義為static final的,它的值是Singleton類型的對象

同樣的,和一般類一樣,后面還可以按照需要,定義類和實例的屬性或者方法。這些都是可以的。

}

Singleton.INSTANCE就是singleton類型的唯一實例了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。