天天看點

十次豔遇單例設計模式(Singleton Pattern)

十次豔遇單例設計模式(Singleton Pattern)

單例設計模式(Singleton Pattern)是最簡單且常見的設計模式之一,在它的核心結構中隻包含一個被稱為單例的特殊類。通過單例模式可以保證系統中一個類隻有一個執行個體而且該執行個體易于外界通路,進而友善對執行個體個數的控制并節約系統資源。如果希望在系統中某個類的對象隻能存在一個,避免多執行個體對象的情況下引起邏輯性錯誤(執行個體化數量可控)單例模式是最好的解決方案。

1、單例類隻能有一個執行個體。

2、單例類必須自己建立自己的唯一執行個體。

3、單例類必須給所有其他對象提供這一執行個體。

Java中,單例模式主要分四種:懶漢式單例、餓漢式單例、登記式單例、ThreadLocal單例模式四種。

懶漢:非線程安全,需要用一定的風騷操作控制,裝逼失敗有可能導緻看一周的海綿寶寶

餓漢:天生線程安全,ClassLoad的時候就已經執行個體化好,該操作過于風騷會造成資源浪費

單例系統資料庫:Spring初始化Bean的時候,預設單例用的就是該方式

單例模式有餓漢模式、懶漢模式、靜态内部類、枚舉等方式實作,這些模式的構造方法是私有的,不可繼承

登記式單例 使得單例對繼承開放

ThreadLocal 是線程副本形式,可以保證局部單例,即在各自的線程中是單例的,但是線程與線程之間不保證單例。

1.特點

私有構造方法,隻能有一個執行個體。

私有靜态引用指向自己執行個體,必須是自己在内部建立的唯一執行個體。

單例類給其它對象提供的都是自己建立的唯一執行個體

2.案例

在計算機系統中,記憶體、線程、CPU等使用情況都可以在任務管理器中看到,但始終隻能打開一個任務管理器,它在Windows作業系統中是具備唯一性的,因為彈多個框多次采集資料浪費性能不說,采集資料存在誤差那就有點逗比了不是麼…

每台電腦隻有一個列印機背景處理程式

線程池的設計一般也是采用單例模式,友善對池中的線程進行控制

3.注意事項
第一種豔遇:

分析:

<col>

在單線程環境一切正常,balancer1和balancer2兩個對象的hashCode一模一樣,由此可以判斷出堆棧中隻有一份内容,不過該代碼塊中存線上程安全隐患,因為缺乏競争條件,多線程環境資源競争的時候就顯得不太樂觀了,請看上文代碼注釋内容

第二種豔遇:無腦上鎖(懶漢)線程安全,性能較差,第一種更新版

毫無疑問,知道synchronized關鍵字的都知道,同步方法在鎖沒釋放之前,其它線程都在排隊候着呢,想不安全都不行啊,但在安全的同時,性能方面就顯得短闆了,我就初始化一次,你丫的每次來都上個鎖,不累的嗎(沒關系,它是為了第三種做鋪墊的)..

第三種豔遇:雙重檢查鎖(DCL),完全就是前兩種的結合體啊,有木有,隻是将同步方法更新成了同步代碼塊

假如沒有volatile情況下産生的問題: 如果第一次檢查loadBalancer不為null,那麼就不需要執行下面的加鎖和初始化操作。是以,可以大幅降低synchronized帶來的性能開銷。線上程執行到第4行,代碼讀取到loadBalancer不為null時,loadBalancer引用的對象有可能還沒有完成初始化。在第7行建立了一個對象,這行代碼可以分解為如下的3行僞代碼:

上面3行代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,如果不了解重排序,後文JMM會詳細解釋)。2和3之間重排序之後的執行時序如下

回到示例代碼第7行,如果發生重排序,另一個并發執行的線程B就有可能在第4行判斷instance不為null。線程B接下來将通路instance所引用的對象,但此時這個對象可能還沒有被A線程初始化,可能導緻NPE。

假設new LazyLoadBalancer()加載内容過多

因重排而導緻loadBalancer提前不為空

正好被其它線程觀察到對象非空直接傳回使用  一種罕見的單例空指針突然來襲

存在問題: 首先我們一定要清楚,DCL是不能保證線程安全的,稍微了解過JVM的就清楚,對比C/C++它始終缺少一個正式的記憶體模型,是以為了提升性能,它還會做一次指令重排操作,這個時候就會導緻loadBalancer提前不為空,正好被其它線程觀察到對象非空直接傳回使用(但實際還有部分内容沒加載完成)

解決方案: 用volatile修飾loadBalancer,因為volatile修飾的成員變量可以確定多個線程都能夠順序處理,它會屏蔽JVM指令重排帶來的性能優化。

第四種豔遇:Demand Holder,靜态内部類 (懶漢)線程安全,推薦使用

在Demand Holder中,我們在LazyLoadBalancer裡增加一個靜态(static)内部類,在該内部類中建立單例對象,再将該單例對象通過getInstance()方法傳回給外部使用,由于靜态單例對象沒有作為LazyLoadBalancer的成員變量直接執行個體化,類加載時并不會執行個體化LoadBalancerHolder,是以既可以實作延遲加載,又可以保證線程安全,不影響系統性能(居家旅行必備良藥啊)

雙重校驗鎖版,不管性能再如何優越,還是使用了synchronized修飾符,既然使用了該修飾符,那麼對性能多多少少都會造成一些影響,于是乎Demand Holder誕生,涉及内部類的加載機制,複習一下,代碼如下:

輸出如下:

是以,我們有如下結論:

第五種豔遇:懶漢式,  防止反射|序列化|反序列化

1.  我們知道 反射可以建立對象,那我們由反射的原理即防止反射破壞了單例,是以誕生了如上文的單例

2.在分布式系統中,有些情況下你需要在單例類中實作 Serializable 接口。這樣你可以在檔案系統中存儲它的狀态并且在稍後的某一時間點取出,為了避免此問題,我們需要提供 readResolve() 方法的實作。readResolve()代替了從流中讀取對象。這就確定了在序列化和反序列化的過程中沒人可以建立新的執行個體

為什麼反序列化可以破壞呢?我們一起來看下ois.readObject()的源碼:

第六種豔遇: 枚舉特性(懶漢)線程安全,推薦使用

相比上一種,該方式同樣是用到了JAVA特性:枚舉類保證隻有一個執行個體(即使使用反射機制也無法多次執行個體化一個枚舉量)

第七種豔遇:餓漢單例(天生線程安全)

利用ClassLoad機制,在加載時進行執行個體化,同時靜态方法隻在編譯期間執行一次初始化,也就隻有一個對象。使用的時候已被初始化完畢可以直接調用,但是相比懶漢模式,它在使用的時候速度最快,但這玩意就像自己挖的坑哭着也得跳,你不用也得初始化一份在記憶體中占個坑… 但是寫着簡單啊~

第八種豔遇:登記式單例

來一把測試:

輸出結果:是線程安全的(ClassA是一個空類,裡面什麼也沒有)

來來 領略一下 Spring的源碼:

登記式單例實際上維護的是一組單例類的執行個體,将這些執行個體存儲到一個Map(登記簿)中,對于已經登記過的單例,則從工廠直接傳回,對于沒有登記的,則先登記,而後傳回

使用map實作系統資料庫;

使用protect修飾構造方法;

有的時候,我們不希望在一開始的時候就把一個類寫成單例模式,但是在運用的時候,我們卻可以像單例一樣使用他

最典型的例子就是spring,他的預設類型就是單例,spring是如何做到把不是單例的類變成單例呢?

這就用到了登記式單例

其實登記式單例并沒有去改變類,他所做的就是起到一個登記的作用,如果沒有登記,他就給你登記,并把生成的執行個體儲存起來,下次你要用的時候直接給你。

IOC容器就是做的這個事,你需要就找他去拿,他就可以很友善的實作Bean的管理。

第九種豔遇: ThreadLocal 局部單例

這種寫法利用了ThreadLocal的特性,可以保證局部單例,即在各自的線程中是單例的,但是線程與線程之間不保證單例。

initialValue()一般是用來在使用時進行重寫的,如果在沒有set的時候就調用get,會調用initialValue方法初始化内容。

ThreadLocal會為每一個線程提供一個獨立的變量副本,進而隔離了多個線程對資料的通路沖突。對于多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊通路,而後者為每一個線程都提供了一份變量,即線程隔離,是以可以同時通路而互不影響。

第十種豔遇: 使用CAS鎖實作(線程安全)

CAS 是線程安全的,使用了無鎖程式設計. 這種方式當在大量線程去擷取執行個體的時候,會造成CPU的激情燃燒~

本文給出了多個版本的單例模式,供我們在項目中使用。實際上,我們在實際項目中一般從豔遇四、五、六中,根據實際情況三選一即可。最後,希望大家有所收獲。

繼續閱讀