單例模式
單例模式來確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。
單例模式確保一個類只有一個實例,並提供一個全局訪問點。
單例模式是一種對象創建型模式。
單例模式又被稱爲單件模式或單態模式。
單例模式的要點有三個:
- 某個類只能有一個實例
- 必須自行創建這個實例
- 必須自行向整個系統提供這個實例
單例模式的經典實現
public class Singleton {
private static Singleton uniqueInstance;//利用一個靜態變量來記錄Singleton類的唯一實例
//這裏有其它的有用實例化變量
private Singleton(){};//私有構造器,只有自己纔可new自己
public static Singleton getInstance(){
if (uniqueInstance == null){//如果uniqueInstance是空的,表示還沒有創建實例
uniqueInstance = new Singleton();//如果uniqueInstance不存在,我們就調用私有構造器生成一個實例
//並把它賦值給uniqueInstance
//如果我們不需要這個實例,那麼這個實例就永遠不會產生,這就是延遲實例化
}
return uniqueInstance;
}
public static void operation(){//類中的其它有用的方法,最好是static的
}
}
模式分析:
- 單例類擁有一個私有構造函數,確保用戶無法通過new關鍵字直接實例化它。
- 該模式還包含一個靜態私有成員變量與靜態共有的工廠方法,該工廠方法負責檢驗實例的存在性並延遲實例化自己,然後存儲在靜態成員的變量中,以確保只有一個實例被創建。
多線程情況下的單例模式
上述單例模式的經典實現,在多線程的情況下是有問題的。
在當線程1還未new 出Singleton對象時線程2判斷爲true,這樣就會導致創建了倆個Singleton對象。
如何避免這種情況呢,只要把getinstance()變成同步方法,多線程的災難就輕而易舉的解決了,如下所示:
public static synchronized Singleton getInstance(){
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
如果你的應用程序可以接受getInstance()造成的額外負擔,直接在方法上加上synchronized 關鍵字是最簡單的實現方式,這可能會造成程序的執行效率大大下降,如果將getInstance()的程序使用在頻繁運行的地方,就必須得重新考慮了。
懶漢式-雙重檢查加鎖
順着上述直接加synchronized 關鍵字同步的思路,我們可以將加鎖的範圍儘量縮小。
public static Singleton getInstance(){
if(instance == null) {//判斷是否有Singleton實例對象
synchronized (Singleton.class) {//類鎖 A處
if(instance == null) {//再次進行判斷 B處
instance = new Singleton();//如果沒有進行創建 C處
}
}
}
return instance ;
}
雙重檢查鎖定背後的理論是:在 B處的第二次檢查可以使得創建兩個不同的 Singleton 對象成爲不可能。假設現在有倆個線程,產生了如下所示的事件序列:
- 線程 1 進入 getInstance() 方法。
- 由於 instance 爲 null,線程 1 在 A處進入 synchronized 塊,獲取Singleton類鎖。
- 線程 1 被線程 2 預佔。
- 線程 2 進入 getInstance() 方法。
- 由於 instance 仍舊爲 null,線程 2 試圖獲取 A處的鎖。然而,由於線程 1 持有該鎖,線程 2 在 A處阻塞。
- 線程 2 被線程 1 預佔。
- 線程 1 執行,由於在 B 處實例仍舊爲 null,線程 1 還創建一個 Singleton 對象並將其引用賦值給 instance。
- 線程 1 退出 synchronized 塊並從 getInstance() 方法返回實例。
- 線程 1 被線程 2 預佔。
- 線程 2 獲取 A 處的鎖並檢查 instance 是否爲 null。
- 由於 instance 是非 null 的,並沒有創建第二個 Singleton 對象,由線程 1 創建的對象被返回。
雙重檢查鎖定背後的理論是完美的。不幸地是,現實完全不同。
雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利運行。
雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺內存模型。內存模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。
無序寫入
爲解釋該問題,需要重新考察上述清單 4 中的C 行。此行代碼創建了一個 Singleton 對象並初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行之前,變量 instance 可能成爲非 null 的。
什麼?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。
假設清單 4 中代碼執行以下事件序列:
- 線程 1 進入 getInstance() 方法。
- 由於 instance 爲 null,線程 1 在 A 處進入 synchronized 塊。
- 線程 1 前進到C 處,但在構造函數執行之前,使實例成爲非 null。
- 線程 1 被線程 2 預佔。
- 線程 2 檢查實例是否爲 null。因爲實例不爲 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton對象。
- 線程 2 被線程 1 預佔。
- 線程 1 通過運行 Singleton 對象的構造函數並將引用返回給它,來完成對該對象的初始化。
對象的初始化,並不是有序的,並不是一次性完成的。
一種可能的情況是,生成了一個實例,但還未將該實例的屬性值初始化(即還未執行構造方法),而這時instance 已經不爲null。所以上述事件序列線程2還未等到線程1將Singleton對象完全初始化完成,得知instance不爲null,便把這個不完全的instance 返回了。線程1繼續執行初始化,然後將完全實例化的instance 返回。這樣線程1與線程2便返回了倆個不完全相同的實例對象。
解決雙重檢查加鎖的問題
解決Singleton 實例對象在不完全初始化的情況下返回而產生了倆種實例的情況,只需要在private volatile static Singleton instance;
加一個volatile 關鍵字即可。這樣就保證了instance對象在多個線程之間的可見性。
但是,現在雙重檢查加鎖的這種方式,現在已經不推薦使用了,那麼現在如何來實現線程安全的單例模式呢?
餓漢式-類初始化模式
public class SingleEHan {
private static final SingleEHan singleEHan = new SingleEHan();
private SingleEHan(){}
public static SingleEHan getInstance(){
return singleEHan;
}
}
在JVM中,對類的加載和初始化,由虛擬機保證線程安全。
但是如果SingleEHan 這個類很大的話,可能會佔據很多的內存空間。可以考慮下面的懶漢式-類初始化模式。
懶漢式-類初始化模式/延遲佔位模式
public class SingleInit {
private SingleInit(){}
//定義一個私有類,來持有當前類的實例
private static class InstanceHolder{
public static SingleInit instance = new SingleInit();
}
public static SingleInit getInstance(){
return InstanceHolder.instance;
}
}
該實現方式,同樣也是利用了JVM保證了類的加載和初始化時的線程安全。
JVM在對SingleInit 進行加載的時候,是不會對私有類進行初始化的,只有當調用getInstance()方法時,JVM纔會將私有類初始化,並由私有類來代替,返回實例出去。
延遲佔位模式其實是個很用處很廣泛的模式,下面舉個栗子~
public class InstanceLazy {
private Integer value;
private Integer val ;//可能是一個很大的對象,也可能是一個非常大的數組,但是這個屬性可能平時用的不是很多,所以可以在需要這個屬性val的時候,再將其進行初始化,採用延遲佔位同時也保證了線程安全。
public InstanceLazy(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
private static class ValHolder {
public static Integer vHolder = new Integer(1000000);
}
public Integer getVal() {
return ValHolder.vHolder;
}
}
單例模式的優缺點
優點:
- 提供了對唯一實例的受控訪問。因爲單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
- 由於在系統內存中只存在一個對象,因此可以節約系統資源,對於一些需要頻繁創建和銷燬的對象,單例模式無疑可以提高系統的性能。
缺點:
- 由於單例模式中沒有抽象層,因此單例類的擴展有很大的困難
- 單例類的職責過重,在一定程度上違背了“單一職責原則”。因爲單例類即充當了工廠角色,提供了工廠方法,又充當了產品角色,包含一些業務方法,將產品的創建和產品本身的功能融合在了一起。
單例模式的適用環境
- 系統只需要一個實例對象的時候
- 系統需要考慮資源消耗太大隻允許創建一個對象
- 客戶端調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。
參考
- 《Head First 設計模式》
- 《軟件體系結構與設計》
- https://blog.csdn.net/chenchaofuck1/article/details/51702129