41丨單例模式上為什么說支持懶加載的雙重檢測不比餓漢式更優(yōu)_W_第1頁
41丨單例模式上為什么說支持懶加載的雙重檢測不比餓漢式更優(yōu)_W_第2頁
41丨單例模式上為什么說支持懶加載的雙重檢測不比餓漢式更優(yōu)_W_第3頁
41丨單例模式上為什么說支持懶加載的雙重檢測不比餓漢式更優(yōu)_W_第4頁
41丨單例模式上為什么說支持懶加載的雙重檢測不比餓漢式更優(yōu)_W_第5頁
已閱讀5頁,還剩13頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)

文檔簡介

1、41 | 單例模式(上):為什么說支持懶加載的雙重檢測不比餓漢式更優(yōu)?2020-02-05 王爭設(shè)計模式之美進入課程講述:馮永吉時長 14:16 大小 11.45M從今天開始,我們正式進入到設(shè)計模式的學(xué)習(xí)。我們知道,經(jīng)典的設(shè)計模式有 23 種。其中,常用的并不是很多。據(jù)我的工作經(jīng)驗來看,常用的可能都不到一半。如果隨便抓一個程序員,讓他說一說最熟悉的 3 種設(shè)計模式,那其中肯定會包含今天要講的單例模式。網(wǎng)上有很多講解單例模式的文章,但大部分都側(cè)重講解,如何來實現(xiàn)一個線程安全的單例。我今天也會講到各種單例的實現(xiàn)方法,但是,這并不是我們專欄學(xué)習(xí)的重點,我重點還是希望帶你搞清楚下面這樣幾個問題(第一個

2、問題會在今天講解,后面三個問題放到下一節(jié)課中 講解)。下載APP為什么要使用單例?單例存在哪些問題? 單例與靜態(tài)類的區(qū)別?有何替代的解決方案?話不多說,讓我們帶著這些問題,正式開始今天的學(xué)習(xí)吧!為什么要使用單例?單例設(shè)計模式(Singleton Design Pattern)理解起來非常簡單。一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類,這種設(shè)計模式就叫作單例設(shè)計模式,簡稱單例模式。對于單例的概念,我覺得沒必要解釋太多,你一看就能明白。我們重點看一下,為什么我們需要單例這種設(shè)計模式?它能解決哪些問題?接下來我通過兩個實戰(zhàn)案例來講解。實戰(zhàn)案例一:處理資源訪問沖突我們先來看第一個例

3、子。在這個例子中,我們自定義實現(xiàn)了一個往文件中打印日志的Logger 類。具體的代碼實現(xiàn)如下所示:復(fù)制代碼12345678910111213141516171819public class Logger private FileWriter writer;public Logger() File file = new File(/Users/wangzheng/log.txt); writer = new FileWriter(file, true); /true表示追加寫入public void log(String message) writer.write(mesasge);/ Logg

4、er類的應(yīng)用示例:public class UserController private Logger logger = new Logger();public void login(String username, String password) / .省略業(yè)務(wù)邏輯代碼.202122232425262728293031logger.log(username + logined!);public class OrderController private Logger logger = new Logger();public void create(OrderVo order) / .省略業(yè)

5、務(wù)邏輯代碼.logger.log(Created an order: + order.toString();看完代碼之后,先別著急看我下面的講解,你可以先思考一下,這段代碼存在什么問題。在上面的代碼中,我們注意到,所有的日志都寫入到同一個文件/Users/wangzheng/log.txt 中。在 UserController 和 OrderController 中,我們分別創(chuàng)建兩個 Logger 對象。在 Web 容器的 Servlet 多線程環(huán)境下,如果兩個 Servlet 線程同時分別執(zhí)行 login() 和 create() 兩個函數(shù),并且同時寫日志到 log.txt 文件中,那就有可

6、能存在日志信息互相覆蓋的情況。為什么會出現(xiàn)互相覆蓋呢?我們可以這么類比著理解。在多線程環(huán)境下,如果兩個線程同時給同一個共享變量加 1,因為共享變量是競爭資源,所以,共享變量最后的結(jié)果有可能并不是加了 2,而是只加了 1。同理,這里的 log.txt 文件也是競爭資源,兩個線程同時往里面寫數(shù)據(jù),就有可能存在互相覆蓋的情況。那如何來解決這個問題呢?我們最先想到的就是通過加鎖的方式:給 log() 函數(shù)加互斥鎖(Java 中可以通過 synchronized 的關(guān)鍵字),同一時刻只允許一個線程調(diào)用執(zhí)行 log()函數(shù)。具體的代碼實現(xiàn)如下所示:復(fù)制代碼1234567891011121314public

7、 class Logger private FileWriter writer;public Logger() File file = new File(/Users/wangzheng/log.txt); writer = new FileWriter(file, true); /true表示追加寫入public void log(String message) synchronized(this) writer.write(mesasge);不過,你仔細想想,這真的能解決多線程寫入日志時互相覆蓋的問題嗎?答案是的。這是因為,這種鎖是一個對象級別的鎖,一個對象在不同的線程下同時調(diào)用 log(

8、) 函數(shù),會被強制要求順序執(zhí)行。但是,不同的對象之間并不共享同一把鎖。在不同的線程下,通過不同的對象調(diào)用執(zhí)行 log() 函數(shù),鎖并不會起作用,仍然有可能存在寫入日志互相覆蓋的問題。我這里稍微補充一下,在剛剛的講解和給出的代碼中,我故意“隱瞞”了一個事實:我們給log() 函數(shù)加不加對象級別的鎖,其實都沒有關(guān)系。因為 FileWriter 本身就是線程安全的, 它的內(nèi)部實現(xiàn)中本身就加了對象級別的鎖,因此,在在外層調(diào)用 write() 函數(shù)的時候,再加對象級別的鎖實際上是多此一舉。因為不同的 Logger 對象不共享 FileWriter 對象,所以,F(xiàn)ileWriter 對象級別的鎖也解決不了

9、數(shù)據(jù)寫入互相覆蓋的問題。那我們該怎么解決這個問題呢?實際上,要想解決這個問題也不難,我們只需要把對象級別的鎖,換成類級別的鎖就可以了。讓所有的對象都共享同一把鎖。這樣就避免了不同對象之間同時調(diào)用 log() 函數(shù),而導(dǎo)致的日志覆蓋問題。具體的代碼實現(xiàn)如下所示:復(fù)制代碼1234567public class Logger private FileWriter writer;public Logger() File file = new File(/Users/wangzheng/log.txt); writer = new FileWriter(file, true); /true表示追加寫入8

10、91011121314public void log(String message) synchronized(Logger.class) / 類級別的鎖writer.write(mesasge);除了使用類級別鎖之外,實際上,解決資源競爭問題的辦法還有很多,分布式鎖是最常聽到的一種解決方案。不過,實現(xiàn)一個安全可靠、無bug、高性能的分布式鎖,并不是件容易的事情。除此之外,并發(fā)隊列(比如 Java 中的 BlockingQueue)也可以解決這個問題: 多個線程同時往并發(fā)隊列里寫日志,一個單獨的線程負責(zé)將并發(fā)隊列中的數(shù)據(jù),寫入到日志文件。這種方式實現(xiàn)起來也稍微有點復(fù)雜。相對于這兩種解決方案,單

11、例模式的解決思路就簡單一些了。單例模式相對于之前類級別鎖的好處是,不用創(chuàng)建那么多 Logger 對象,一方面節(jié)省內(nèi)存空間,另一方面節(jié)省系統(tǒng)文件句柄(對于操作系統(tǒng)來說,文件句柄也是一種資源,不能隨便浪費)。我們將 Logger 設(shè)計成一個單例類,程序中只允許創(chuàng)建一個 Logger 對象,所有的線程共享使用的這一個 Logger 對象,共享一個 FileWriter 對象,而 FileWriter 本身是對象級別線程安全的,也就避免了多線程情況下寫日志會互相覆蓋的問題。按照這個設(shè)計思路,我們實現(xiàn)了 Logger 單例類。具體代碼如下所示:復(fù)制代碼12345678910111213141516pub

12、lic class Logger privateprivateFileWriter writer;static final Logger instance = new Logger();privateLogger() File file = new File(/Users/wangzheng/log.txt); writer = new FileWriter(file, true); /true表示追加寫入public static Logger getInstance() return instance;public void log(String message) writer.write

13、(mesasge);171819202122232425262728293031323334/ Logger類的應(yīng)用示例:public class UserController public void login(String username, String password) / . 省 略 業(yè) 務(wù) 邏 輯 代 碼 . Logger.getInstance().log(username + logined!);public class OrderController private Logger logger = new Logger();public void create(OrderV

14、o order) / .省略業(yè)務(wù)邏輯代碼.Logger.getInstance().log(Created a order: + order.toString();實戰(zhàn)案例二:表示全局唯一類從業(yè)務(wù)概念上,如果有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份,那就比較適合設(shè)計為單例類。比如,配置信息類。在系統(tǒng)中,我們只有一個配置文件,當(dāng)配置文件被加載到內(nèi)存之后,以對象的形式存在,也理所應(yīng)當(dāng)只有一份。再比如,唯一遞增 ID 號碼生成器(第 34 講中我們講的是唯一 ID 生成器,這里講的是唯一遞增 ID 生成器),如果程序中有兩個對象,那就會存在生成重復(fù) ID 的情況,所以,我們應(yīng)該將 ID 生成器類設(shè)計為單例。復(fù)制

15、代碼1234567891011121314import java.util.concurrent.atomic.AtomicLong; public class IdGenerator / AtomicLong是一個Java并發(fā)庫中提供的一個原子變量類型,/ 它將一些線程不安全需要加鎖的復(fù)合操作封裝為了線程安全的原子操作,/ 比如下面會用到的incrementAndGet().private privateprivateAtomicLong id = new AtomicLong(0);static final IdGenerator instance = new IdGenerator();

16、 IdGenerator() public static IdGenerator getInstance() return instance;public long getId() return id.incrementAndGet();15161718/ IdGenerator使用舉例long id = IdGenerator.getInstance().getId();實際上,今天講到的兩個代碼實例(Logger、IdGenerator),設(shè)計的都并不優(yōu)雅,還存在一些問題。至于有什么問題以及如何改造,今天我暫時賣個關(guān)子,下一節(jié)課我會詳細講解。如何實現(xiàn)一個單例?盡管介紹如何實現(xiàn)一個單例模式的

17、文章已經(jīng)有很多了,但為了保證內(nèi)容的完整性,我這里還是簡單介紹一下幾種經(jīng)典實現(xiàn)方式。概括起來,要實現(xiàn)一個單例,我們需要關(guān)注的點無外乎下面幾個:構(gòu)造函數(shù)需要是 private 訪問權(quán)限的,這樣才能避免外部通過 new 創(chuàng)建實例;考慮對象創(chuàng)建時的線程安全問題; 考慮是否支持延遲加載;考慮 getInstance() 性能是否高(是否加鎖)。如果你對這塊已經(jīng)很熟悉了,你可以當(dāng)作復(fù)習(xí)。注意,下面的幾種單例實現(xiàn)方式是針對Java 語言語法的,如果你熟悉的是其他語言,不妨對比 Java 的這幾種實現(xiàn)方式,自己試著總結(jié)一下,利用你熟悉的語言,該如何實現(xiàn)。1. 餓漢式餓漢式的實現(xiàn)方式比較簡單。在類加載的時候,i

18、nstance靜態(tài)實例就已經(jīng)創(chuàng)建并初始化好了,所以,instance 實例的創(chuàng)建過程是線程安全的。不過,這樣的實現(xiàn)方式不支持延遲加載(在真正用到 IdGenerator 的時候,再創(chuàng)建實例),從名字中我們也可以看出這一點。具體的代碼實現(xiàn)如下所示:復(fù)制代碼1234public class IdGenerator private AtomicLong id = new AtomicLong(0);private static final IdGenerator instance = new IdGenerator(); private IdGenerator() 567891011public s

19、tatic IdGenerator getInstance() return instance;public long getId() return id.incrementAndGet();有人覺得這種實現(xiàn)方式不好,因為不支持延遲加載,如果實例占用資源多(比如占用內(nèi)存多)或初始化耗時長(比如需要加載各種配置文件),提前初始化實例是一種浪費資源的行為。最好的方法應(yīng)該在用到的時候再去初始化。不過,我個人并不認同這樣的觀點。如果初始化耗時長,那我們最好不要等到真正要用它的時候,才去執(zhí)行這個耗時長的初始化過程,這會影響到系統(tǒng)的性能(比如,在響應(yīng)客戶端接口請求的時候,做這個初始化操作, 會導(dǎo)致此請求的

20、響應(yīng)時間變長,甚至超時)。采用餓漢式實現(xiàn)方式,將耗時的初始化操作, 提前到程序啟動的時候完成,這樣就能避免在程序運行的時候,再去初始化導(dǎo)致的性能問題。如果實例占用資源多,按照 fail-fast 的設(shè)計原則(有問題及早暴露),那我們也希望在程序啟動時就將這個實例初始化好。如果資源不夠,就會在程序啟動的時候觸發(fā)報錯(比如Java 中的 PermGen Space OOM),我們可以立即去修復(fù)。這樣也能避免在程序運行一段時間后,突然因為初始化這個實例占用資源過多,導(dǎo)致系統(tǒng),影響系統(tǒng)的可用性。2. 懶漢式有餓漢式,對應(yīng)地,就有懶漢式。懶漢式相對于餓漢式的優(yōu)勢是支持延遲加載。具體的代碼實現(xiàn)如下所示:復(fù)

21、制代碼1234567891011public class IdGenerator private privateprivateAtomicLong id = new AtomicLong(0); static IdGenerator instance;IdGenerator() public static synchronized IdGenerator getInstance() if (instance = null) instance = new IdGenerator();return instance;public long getId() 121314return id.incre

22、mentAndGet();不過懶漢式的缺點也很明顯,我們給 getInstance() 這個方法加了一把大鎖(synchronzed),導(dǎo)致這個函數(shù)的并發(fā)度很低。量化一下的話,并發(fā)度是 1,也就相當(dāng)于串行操作了。而這個函數(shù)是在單例使用期間,一直會被調(diào)用。如果這個單例類偶爾會被用3. 雙重檢測餓漢式不支持延遲加載,懶漢式有性能問題,不支持高并發(fā)。那我們再來看一種既支持延遲加載、又支持高并發(fā)的單例實現(xiàn)方式,也就是雙重檢測實現(xiàn)方式。在這種實現(xiàn)方式中,只要 instance 被創(chuàng)建之后,即便再調(diào)用 getInstance() 函數(shù)也不會再進入到加鎖邏輯中了。所以,這種實現(xiàn)方式解決了懶漢式并發(fā)度低的問題

23、。具體的代碼實現(xiàn)如下所示:復(fù)制代碼1 public class IdGenerator 2 private AtomicLong id = new AtomicLong(0);3 private static IdGenerator instance;4 private IdGenerator() 5 public static IdGenerator getInstance() 6 if (instance = null) 7 synchronized(IdGenerator.class) / 此處為類級別的鎖8 if (instance = null) 9 instance = new I

24、dGenerator();10111213return instance;1415 public long getId() 16 return id.incrementAndGet();1718 網(wǎng)上有人說,這種實現(xiàn)方式有些問題。因為指令重排序,可能會導(dǎo)致 IdGenerator 對象被new 出來,并且賦值給 instance 之后,還沒來得及初始化(執(zhí)行構(gòu)造函數(shù)中的代碼邏輯),就被另一個線程使用了。要解決這個問題,我們需要給 instance 成員變量加上 volatile 關(guān)鍵字,禁止指令重排序才行。實際上,只有很低版本的 Java 才會有這個問題。我們現(xiàn)在用的高版本的 Java 已經(jīng)在

25、JDK 內(nèi)部實現(xiàn)中解決了這個問題(解決的方法很簡單,只要把對象 new 操作和初始化操作設(shè)計為原子操作,就自然能禁止重排序)。關(guān)于這點的詳細解釋,跟特定語言有關(guān),我就不展開講了,感興趣的同學(xué)可以自行研究一下。4. 靜態(tài)內(nèi)部類我們再來看一種比雙重檢測更加簡單的實現(xiàn)方法,那就是利用 Java 的靜態(tài)內(nèi)部類。它有點類似餓漢式,但又能做到了延遲加載。具體是怎么做到的呢?我們先來看它的代碼實現(xiàn)。復(fù)制代碼12345678910111213141516public class IdGenerator privateprivateAtomicLong id = new AtomicLong(0);IdGene

26、rator() privatestatic class SingletonHolderprivate static final IdGenerator instance = new IdGenerator();public static IdGenerator getInstance() return SingletonHolder.instance;public long getId() return id.incrementAndGet();SingletonHolder 是一個靜態(tài)內(nèi)部類,當(dāng)外部類 IdGenerator 被加載的時候,并不會創(chuàng)建SingletonHolder 實例對象。

27、只有當(dāng)調(diào)用 getInstance() 方法時,SingletonHolder 才會被加載,這個時候才會創(chuàng)建 instance。insance 的唯一性、創(chuàng)建過程的線程安全性,都由JVM 來保證。所以,這種實現(xiàn)方法既保證了線程安全,又能做到延遲加載。5. 枚舉最后,我們介紹一種最簡單的實現(xiàn)方式,基于枚舉類型的單例實現(xiàn)。這種實現(xiàn)方式通過Java 枚舉類型本身的特性,保證了實例創(chuàng)建的線程安全性和實例的唯一性。具體的代碼如下所示:復(fù)制代碼12345678public enum IdGenerator INSTANCE;private AtomicLong id = new AtomicLong(0)

28、;public long getId() return id.incrementAndGet();重點回顧好了,今天的內(nèi)容到此就講完了。我們來總結(jié)回顧一下,你需要掌握的重點內(nèi)容。1. 單例的定義單例設(shè)計模式(Singleton Design Pattern)理解起來非常簡單。一個類只允許創(chuàng)建一個對象(或者叫實例),那這個類就是一個單例類,這種設(shè)計模式就叫作單例設(shè)計模式,簡稱單例模式。2. 單例的用處從業(yè)務(wù)概念上,有些數(shù)據(jù)在系統(tǒng)中只應(yīng)該保存一份,就比較適合設(shè)計為單例類。比如,系統(tǒng)的配置信息類。除此之外,我們還可以使用單例解決資源訪問沖突的問題。3. 單例的實現(xiàn)單例有下面幾種經(jīng)典的實現(xiàn)方式。餓漢式

29、餓漢式的實現(xiàn)方式,在類加載的期間,就已經(jīng)將 instance 靜態(tài)實例初始化好了,所以,instance 實例的創(chuàng)建是線程安全的。不過,這樣的實現(xiàn)方式不支持延遲加載實例。懶漢式懶漢式相對于餓漢式的優(yōu)勢是支持延遲加載。這種實現(xiàn)方式會導(dǎo)致頻繁加鎖、釋放鎖,以及并發(fā)度問題,頻繁的調(diào)用會產(chǎn)生性能瓶頸。雙重檢測雙重檢測實現(xiàn)方式既支持延遲加載、又支持高并發(fā)的單例實現(xiàn)方式。只要 instance 被創(chuàng)建之后,再調(diào)用 getInstance() 函數(shù)都不會進入到加鎖邏輯中。所以,這種實現(xiàn)方式解決了懶漢式并發(fā)度低的問題。靜態(tài)內(nèi)部類利用 Java 的靜態(tài)內(nèi)部類來實現(xiàn)單例。這種實現(xiàn)方式,既支持延遲加載,也支持高并發(fā)

30、,實現(xiàn)起來也比雙重檢測簡單。枚舉最簡單的實現(xiàn)方式,基于枚舉類型的單例實現(xiàn)。這種實現(xiàn)方式通過 Java 枚舉類型本身的特性,保證了實例創(chuàng)建的線程安全性和實例的唯一性。課堂討論1. 在你所熟悉的編程語言的類庫中,有哪些類是單例類?又為什么要設(shè)計成單例類呢?2. 在第一個實戰(zhàn)案例中,除了我們講到的類級別鎖、分布式鎖、并發(fā)隊列、單例模式等解 決方案之外,實際上還有一種非常簡單的解決日志互相覆蓋問題的方法,你想到了嗎?可以在留言區(qū)說一說,和同學(xué)一起交流和分享。如果有收獲,也歡迎你把這篇文章分享給你的朋友。 版權(quán)歸極客邦科技所有,未經(jīng)許可不得傳播售賣。 頁面已增加防盜追蹤,如有侵權(quán)極客邦將依法追究其法律責(zé)

31、任。上一篇40 | 運用學(xué)過的設(shè)計原則和思想完善之前講的性能計數(shù)器項目(下)下一篇42 | 單例模式(中):我為什么不推薦使用單例模式?又有何替代方案?精選留言 (27)寫留言aof置頂2020-02-05這真的是看過的關(guān)于講單例的最好的文章展開12Douglas2020-02-05爭哥新年好, 有個問題想請教一下,單例的實現(xiàn)中看到過一種實現(xiàn)方式,包括在spring源碼中有類似的實現(xiàn) ,代碼如下1. public class Singleton private static volatile Singleton instance=null;private Singleton() 展開56辣么大

32、2020-02-05簡單的方法:創(chuàng)建一個靜態(tài)私有的filewritter,多線程或者多個Logger對象共享一個filewr itter。4黃林晴2020-02-05打卡看過Eventbus 的源碼,寫法是典型的雙重鎖檢驗方式,但是構(gòu)造方法是public 的看源碼解釋,這是因為EventBus可能有多條總線,訂閱者注冊到不同線上的 EventBus,展開23憶水寒2020-02-05第一個問題,在我的項目中緩存類的節(jié)點設(shè)置為單例模式,還有加載全局配置文件的類, 也設(shè)置為了單例模式。第二個問題,我是用消息隊列實現(xiàn)的日志收集。22020-02-05這篇非常棒,2小晏子2020-02-051. JD

33、K中 java.lang.Runtime是單例實現(xiàn)的,該類用于管理應(yīng)用程序運行期間的各種信息, 比如memory和processor信息,所以從該類的用途可以知道該類必須是單例的。2. 使用多個文件,每new一個實例就使用一個新文件,這樣就沒有文件覆蓋問題了。展開2zs阿帥2020-02-06爭哥,如果服務(wù)是多個實例跑,日志那個單例模式會導(dǎo)致覆蓋嗎?1沈康2020-02-061、java bean大部分都是單例吧,例如我們的service bean,單例也是為了復(fù)用類和資源共享吧,但是要注意單例需要無狀態(tài),有狀態(tài)的則要考慮線程安全問題2、如果線程安全的話,共享一個類就可以了,依賴注入展開1Summer 空城2020-02-05王老師這篇講解的非常棒,贊!展開1xindoo2020-02-10為什么高版本的jdk單例不再需要volatile修飾,求詳細參考資料,感謝中的成大大2020-02-10為何在講本文中實戰(zhàn)一ublic class OrderController private Logger logger = new Logger(); public void crea te(OrderVo order) / .省略業(yè)務(wù)邏輯代碼. Logger.getInstance().l

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論