深入理解Java中沒(méi)那么簡(jiǎn)單的單例模式
前言
大家都知道關(guān)于Java中單例(Singleton)模式是一種廣泛使用的設(shè)計(jì)模式。單例模式的主要作用是保證在Java程序中,某個(gè)類只有一個(gè)實(shí)例存在。一些管理器和控制器常被設(shè)計(jì)成單例模式。
單例模式有很多好處,它能夠避免實(shí)例對(duì)象的重復(fù)創(chuàng)建,不僅可以減少每次創(chuàng)建對(duì)象的時(shí)間開銷,還可以節(jié)約內(nèi)存空間;能夠避免由于操作多個(gè)實(shí)例導(dǎo)致的邏輯錯(cuò)誤。如果一個(gè)對(duì)象有可能貫穿整個(gè)應(yīng)用程序,而且起到了全局統(tǒng)一管理控制的作用,那么單例模式也許是一個(gè)值得考慮的選擇。
單例模式有很多種寫法,大部分寫法都或多或少有一些不足。下面將分別對(duì)這幾種寫法進(jìn)行介紹。
1、餓漢模式
public class Singleton{ private static Singleton instance = new Singleton(); private Singleton(){} public static Singleton newInstance(){ return instance; } }
從代碼中我們看到,類的構(gòu)造函數(shù)定義為private的,保證其他類不能實(shí)例化此類,然后提供了一個(gè)靜態(tài)實(shí)例并返回給調(diào)用者。餓漢模式是最簡(jiǎn)單的一種實(shí)現(xiàn)方式,餓漢模式在類加載的時(shí)候就對(duì)實(shí)例進(jìn)行創(chuàng)建,實(shí)例在整個(gè)程序周期都存在。它的好處是只在類加載的時(shí)候創(chuàng)建一次實(shí)例,不會(huì)存在多個(gè)線程創(chuàng)建多個(gè)實(shí)例的情況,避免了多線程同步的問(wèn)題。它的缺點(diǎn)也很明顯,即使這個(gè)單例沒(méi)有用到也會(huì)被創(chuàng)建,而且在類加載之后就被創(chuàng)建,內(nèi)存就被浪費(fèi)了。
這種實(shí)現(xiàn)方式適合單例占用內(nèi)存比較小,在初始化時(shí)就會(huì)被用到的情況。但是,如果單例占用的內(nèi)存比較大,或單例只是在某個(gè)特定場(chǎng)景下才會(huì)用到,使用餓漢模式就不合適了,這時(shí)候就需要用到懶漢模式進(jìn)行延遲加載。
2、懶漢模式
public class Singleton{ private static Singleton instance = null; private Singleton(){} public static Singleton newInstance(){ if(null == instance){ instance = new Singleton(); } return instance; } }
懶漢模式中單例是在需要的時(shí)候才去創(chuàng)建的,如果單例已經(jīng)創(chuàng)建,再次調(diào)用獲取接口將不會(huì)重新創(chuàng)建新的對(duì)象,而是直接返回之前創(chuàng)建的對(duì)象。如果某個(gè)單例使用的次數(shù)少,并且創(chuàng)建單例消耗的資源較多,那么就需要實(shí)現(xiàn)單例的按需創(chuàng)建,這個(gè)時(shí)候使用懶漢模式就是一個(gè)不錯(cuò)的選擇。但是這里的懶漢模式并沒(méi)有考慮線程安全問(wèn)題,在多個(gè)線程可能會(huì)并發(fā)調(diào)用它的getInstance()方法,導(dǎo)致創(chuàng)建多個(gè)實(shí)例,因此需要加鎖解決線程同步問(wèn)題,實(shí)現(xiàn)如下。
public class Singleton{ private static Singleton instance = null; private Singleton(){} public static synchronized Singleton newInstance(){ if(null == instance){ instance = new Singleton(); } return instance; } }
3、雙重校驗(yàn)鎖
加鎖的懶漢模式看起來(lái)即解決了線程并發(fā)問(wèn)題,又實(shí)現(xiàn)了延遲加載,然而它存在著性能問(wèn)題,依然不夠完美。synchronized修飾的同步方法比一般方法要慢很多,如果多次調(diào)用getInstance()
,累積的性能損耗就比較大了。因此就有了雙重校驗(yàn)鎖,先看下它的實(shí)現(xiàn)代碼。
public class Singleton { private static Singleton instance = null; private Singleton(){} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) {//2 instance = new Singleton(); } } } return instance; } }
可以看到上面在同步代碼塊外多了一層instance為空的判斷。由于單例對(duì)象只需要?jiǎng)?chuàng)建一次,如果后面再次調(diào)用getInstance()
只需要直接返回單例對(duì)象。因此,大部分情況下,調(diào)用getInstance()都不會(huì)執(zhí)行到同步代碼塊,從而提高了程序性能。不過(guò)還需要考慮一種情況,假如兩個(gè)線程A、B,A執(zhí)行了if (instance == null)
語(yǔ)句,它會(huì)認(rèn)為單例對(duì)象沒(méi)有創(chuàng)建,此時(shí)線程切到B也執(zhí)行了同樣的語(yǔ)句,B也認(rèn)為單例對(duì)象沒(méi)有創(chuàng)建,然后兩個(gè)線程依次執(zhí)行同步代碼塊,并分別創(chuàng)建了一個(gè)單例對(duì)象。為了解決這個(gè)問(wèn)題,還需要在同步代碼塊中增加if (instance == null)
語(yǔ)句,也就是上面看到的代碼2。
我們看到雙重校驗(yàn)鎖即實(shí)現(xiàn)了延遲加載,又解決了線程并發(fā)問(wèn)題,同時(shí)還解決了執(zhí)行效率問(wèn)題,是否真的就萬(wàn)無(wú)一失了呢?
這里要提到Java中的指令重排優(yōu)化。所謂指令重排優(yōu)化是指在不改變?cè)Z(yǔ)義的情況下,通過(guò)調(diào)整指令的執(zhí)行順序讓程序運(yùn)行的更快。JVM中并沒(méi)有規(guī)定編譯器優(yōu)化相關(guān)的內(nèi)容,也就是說(shuō)JVM可以自由的進(jìn)行指令重排序的優(yōu)化。
這個(gè)問(wèn)題的關(guān)鍵就在于由于指令重排優(yōu)化的存在,導(dǎo)致初始化Singleton和將對(duì)象地址賦給instance字段的順序是不確定的。在某個(gè)線程創(chuàng)建單例對(duì)象時(shí),在構(gòu)造方法被調(diào)用之前,就為該對(duì)象分配了內(nèi)存空間并將對(duì)象的字段設(shè)置為默認(rèn)值。此時(shí)就可以將分配的內(nèi)存地址賦值給instance字段了,然而該對(duì)象可能還沒(méi)有初始化。若緊接著另外一個(gè)線程來(lái)調(diào)用getInstance,取到的就是狀態(tài)不正確的對(duì)象,程序就會(huì)出錯(cuò)。
以上就是雙重校驗(yàn)鎖會(huì)失效的原因,不過(guò)還好在JDK1.5及之后版本增加了volatile關(guān)鍵字。volatile的一個(gè)語(yǔ)義是禁止指令重排序優(yōu)化,也就保證了instance變量被賦值的時(shí)候?qū)ο笠呀?jīng)是初始化過(guò)的,從而避免了上面說(shuō)到的問(wèn)題。
代碼如下:
public class Singleton { private static volatile Singleton instance = null; private Singleton(){} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
4、靜態(tài)內(nèi)部類
除了上面的三種方式,還有另外一種實(shí)現(xiàn)單例的方式,通過(guò)靜態(tài)內(nèi)部類來(lái)實(shí)現(xiàn)。首先看一下它的實(shí)現(xiàn)代碼:
public class Singleton{ private static class SingletonHolder{ public static Singleton instance = new Singleton(); } private Singleton(){} public static Singleton newInstance(){ return SingletonHolder.instance; } }
這種方式同樣利用了類加載機(jī)制來(lái)保證只創(chuàng)建一個(gè)instance實(shí)例。它與餓漢模式一樣,也是利用了類加載機(jī)制,因此不存在多線程并發(fā)的問(wèn)題。不一樣的是,它是在內(nèi)部類里面去創(chuàng)建對(duì)象實(shí)例。這樣的話,只要應(yīng)用中不使用內(nèi)部類,JVM就不會(huì)去加載這個(gè)單例類,也就不會(huì)創(chuàng)建單例對(duì)象,從而實(shí)現(xiàn)懶漢式的延遲加載。也就是說(shuō)這種方式可以同時(shí)保證延遲加載和線程安全。
5、枚舉
再來(lái)看本文要介紹的最后一種實(shí)現(xiàn)方式:枚舉。
public enum Singleton{ instance; public void whateverMethod(){} }
面提到的四種實(shí)現(xiàn)單例的方式都有共同的缺點(diǎn):
1)需要額外的工作來(lái)實(shí)現(xiàn)序列化,否則每次反序列化一個(gè)序列化的對(duì)象時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例。
2)可以使用反射強(qiáng)行調(diào)用私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)。
而枚舉類很好的解決了這兩個(gè)問(wèn)題,使用枚舉除了線程安全和防止反射調(diào)用構(gòu)造器之外,還提供了自動(dòng)序列化機(jī)制,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象。因此,《Effective Java》作者推薦使用的方法。不過(guò),在實(shí)際工作中,很少看見有人這么寫。
總結(jié)
本文總結(jié)了五種Java中實(shí)現(xiàn)單例的方法,其中前兩種都不夠完美,雙重校驗(yàn)鎖和靜態(tài)內(nèi)部類的方式可以解決大部分問(wèn)題,平時(shí)工作中使用的最多的也是這兩種方式。枚舉方式雖然很完美的解決了各種問(wèn)題,但是這種寫法多少讓人感覺有些生疏。個(gè)人的建議是,在沒(méi)有特殊需求的情況下,使用第三種和第四種方式實(shí)現(xiàn)單例模式。
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流。
相關(guān)文章
用intellij Idea加載eclipse的maven項(xiàng)目全流程(圖文)
這篇文章主要介紹了用intellij Idea加載eclipse的maven項(xiàng)目全流程(圖文),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12詳解在SpringBoot應(yīng)用中獲取應(yīng)用上下文方法
本篇文章主要介紹了詳解在SpringBoot應(yīng)用中獲取應(yīng)用上下文方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04如何解決Webservice第一次訪問(wèn)特別慢的問(wèn)題
這篇文章主要介紹了如何解決Webservice第一次訪問(wèn)特別慢的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06SpringBoot啟動(dòng)報(bào)錯(cuò)Failed to determine a suitable driver class
這篇文章主要介紹了SpringBoot啟動(dòng)報(bào)錯(cuò)Failed to determine a suitable driver class,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05spring?boot實(shí)現(xiàn)圖片上傳到后臺(tái)的功能(瀏覽器可直接訪問(wèn))
這篇文章主要介紹了spring?boot實(shí)現(xiàn)圖片上傳到后臺(tái)的功能(瀏覽器可直接訪問(wèn)),需要的朋友可以參考下2022-04-04Spring boot 集成 Druid 數(shù)據(jù)源過(guò)程詳解
這篇文章主要介紹了Spring boot 集成 Druid 數(shù)據(jù)源過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08java狀態(tài)機(jī)方案解決訂單狀態(tài)扭轉(zhuǎn)示例詳解
這篇文章主要為大家介紹了java狀態(tài)機(jī)方案解決訂單狀態(tài)扭轉(zhuǎn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03