精雕細琢 全方位解析單例模式

2022-07-04 10:00:20 字數 3609 閱讀 1202

單例模式有的時候特別重要,因為某些系統是要求某個類在整個生命週期中有且只有乙個例項存在,這時候就要用到單例模式。

保證乙個類僅有乙個例項,並提供乙個訪問它的全域性訪問點。

單例模式也是建立型設計模式。

我們一步步研究。

按照慣例,先講故事。

各個大學想請史上最牛科學家來自己學校講課。

分析一波,既然是史上最牛科學家,那麼就代表只有乙個人,因為並沒有說「之一」二字,帶了「之一」的話那就是一批人。所以這個人假設就叫edward,就是史上最牛科學家的唯一例項。各個大學現在要來搶他,不可能把他拆了,必須按照人家的檔期來安排。

那麼到底如何請呢?

private static bestscientist edward = null;

private bestscientist(){}

public static bestscientist getinstance()

return edward;

}

我們觀察,上面單例模式的定義說得清楚,只提供乙個訪問它的全域性訪問點,那麼其他內部的東西都要被封裝。所以edward作為靜態變數是private的。只留了乙個getinstance()方法作為全域性訪問點。

要注意,把建構函式重寫改為private,將外部直接建立例項的機會堵住。

為什麼是static?

因為只有變數定義為static,它才是屬於類的,而不是屬於某個例項,它在類載入的時候就能被初始化,這個變數才是唯一的。

而方法只有是靜態的,才可以直接使用類去呼叫而不必先建立例項。

bestscientist.getinstance();// edward就來了。
缺陷:

不能滿足併發請求,當乙個執行緒進來的時候剛判斷完edward是空,這時候另乙個執行緒卻剛剛完成建立,那麼第乙個執行緒因為已經過了空值判斷的約束,就會再建立乙個例項。那就亂了,相當於出來了乙個edward的複製人。

private static bestscientist edward = new bestscientist();

private bestscientist(){}

public static bestscientist getinstance()

這個版本也叫餓漢版,就是著急,類載入的時候就把唯一例項建立完了,其他人來用的時候就過來取就行了,系統永遠不會出現多個例項。

private static bestscientist edward = null;

private bestscientist(){}

synchronized public static bestscientist getinstance()

return edward;

}

也叫懶漢式,就是不著急,例項的建立被延遲到使用的時候,正如我們在瀏覽器中滑到那個位置才開始載入一樣,這樣可以減少系統負擔。這基本就是第一版的併發版本,加了把鎖強制多執行緒時單個執行緒獲得鎖獨享操作,其他執行緒等待。鎖的位置也可以移到方法體內部。

private static bestscientist edward = null;

private bestscientist(){}

public static bestscientist getinstance()

}return edward;

}

貌似更精確,但是問題又出現了,當乙個執行緒獲得鎖開始建立例項的時候,另外好幾個執行緒都經過了null判斷阻塞在馬上建立例項的前面,一旦鎖被釋放,將建立多個例項出來。

private volatile static bestscientist edward = null;

private bestscientist(){}

public static bestscientist getinstance()}}

return edward;

}

鎖內再加一層null判斷即可解決上面的問題。這就是double-check locking。懶漢式的最終版本。

什麼是volatile?

volatile關鍵字是多執行緒程式設計中常常用到的,他的效果與鎖很類似,但是實現方式卻有不同。這裡簡單介紹一下計算機記憶體的結構,計算機記憶體結構分為堆疊,堆(heap)是計算機主記憶體,而棧(stack)多是一些變數,日常我們使用的時候,都是直接操作這個變數,變數會在被操作結束以後寫回主記憶體。而多執行緒系統在這種方式下就會出現問題,因為變數在被乙個執行緒未操作完之前,被另乙個或多個執行緒也做了修改,那麼就看誰是最後乙個修改的,最終只有這乙個修改被寫回主記憶體,造成其他執行緒寫入的修改無效的bug。這時候,使用volatile關鍵字修飾該變數,會讓該變數的每次修改都寫回主記憶體,其他執行緒均等待其修改完成以後再寫入,以保證變數被操作時的同步性。

我們發現餓漢式和懶漢式各有利弊,餓漢式非常簡單,但是占用了系統資源,而懶漢式雖然可以做到用的時候再建立,但是需要很多判斷,還有鎖機制,會影響效能。那麼下面將使用的是餓漢懶漢合二為一,克服了兩者各自的缺點,整合了兩者的優點,那就是iodh(在軍需官那裡初始化),軍需官就是classholder。

private bestscientist(){}

private static class classholder

}public static bestscientist getinstance()

外部仍舊是通過

bestscientist.getinstance();// edward就來了。
獲取例項,但是內部實現卻大不一樣。這裡去掉了鎖,增加了乙個static class,這個類的建構函式裡定義了乙個靜態變數並初始化建立例項。最終效果是,當bestscientist類載入時,其靜態內部類classholder並沒有任何動作,classholder是什麼時候載入的呢?是在getinstance方法中獲取classholder內部靜態變數的時候載入,而隨著classholder被載入,其靜態變數會跟著一起初始化。這可以稱得上完美,盡取以上餓漢懶漢優勢而摒棄雙方劣勢。

為什麼是static class?

static關鍵字有兩個特性,上面已經提到,這裡再重申一次

static所有針對的內容都是類,它是屬於類的而不是例項。所以,反過來說,類被載入的時候就會同時執行static修飾的內容,類載入是在類被使用的時候,使用完了以後會按照gc機制清除。

擴充套件問題

有些人可能覺得單例類的職責過重,既有工廠角色又有產品角色,但我不這麼認為,單例類的內部結構在iodh就趨於非常穩定的狀態,不會有什麼擴充套件,所以不必去增加抽象層,而它的職責就是提供唯一例項,這是個高內聚的類,所以我認為並不違反「單一職責原則」,就看你在哪乙個層面看問題了。

注意上面提到了類和類的例項在使用完畢以後,會按照gc機制去清除,那麼可能會造成共享例項被清除的後果。但是gc預設的機制是按照使用計數器,當這個例項沒有任何執行緒去使用的時候,它的清除級別也是較低的,即使被清除了,再在使用的時候去建立也未嘗不可,這比起其他版本的開銷來說已經非常高效。

單例模式解析

1 餓漢模式 優點 在類初始化時已經例項化完成,呼叫時較快 執行緒安全 缺點 不管後期是否使用,都進行了一次初始化,有可能浪費資源 public class singleton 建構函式私有化 public static singleton getinstance 2 懶漢模式 不推薦 優點 只有在...

單例模式解析

什麼是單例模式?在整個jvm執行週期中只有乙個例項的物件。用法 執行緒安全 double check public void class singleton 3.實現雙重檢查鎖構造單例 public singleton getinstance return instance 為什麼要使用雙重檢查鎖?...

單例模式完全解析

本文將 單例模式的各種情況,並給出相應的建議。單例模式應該是設計模式中比較簡單的乙個,但是在多執行緒併發的環境下使用卻是不那麼簡單了。首先看最原始的單例模式。顯然這個寫法在單執行緒環境下非常好,但是多執行緒會導致多個例項出現,這個大家都能理解。最簡單的改造方式是新增乙個同步鎖。顯然上面的方法避免了併...