JAVA多線程并發(fā)下的單例模式應(yīng)用
單例模式應(yīng)該是設(shè)計(jì)模式中比較簡(jiǎn)單的一個(gè),也是非常常見(jiàn)的,但是在多線程并發(fā)的環(huán)境下使用卻是不那么簡(jiǎn)單了,今天給大家分享一個(gè)我在開(kāi)發(fā)過(guò)程中遇到的單例模式的應(yīng)用。
首先我們先來(lái)看一下單例模式的定義:
一個(gè)類有且僅有一個(gè)實(shí)例,并且自行實(shí)例化向整個(gè)系統(tǒng)提供。
單例模式的要素:
1.私有的靜態(tài)的實(shí)例對(duì)象
2.私有的構(gòu)造函數(shù)(保證在該類外部,無(wú)法通過(guò)new的方式來(lái)創(chuàng)建對(duì)象實(shí)例)
3.公有的、靜態(tài)的、訪問(wèn)該實(shí)例對(duì)象的方法
單例模式分為懶漢形和餓漢式
懶漢式:
應(yīng)用剛啟動(dòng)的時(shí)候,并不創(chuàng)建實(shí)例,當(dāng)外部調(diào)用該類的實(shí)例或者該類實(shí)例方法的時(shí)候,才創(chuàng)建該類的實(shí)例。(時(shí)間換空間)
優(yōu)點(diǎn):實(shí)例在被使用的時(shí)候才被創(chuàng)建,可以節(jié)省系統(tǒng)資源,體現(xiàn)了延遲加載的思想。
缺點(diǎn):由于系統(tǒng)剛啟動(dòng)時(shí)且未被外部調(diào)用時(shí),實(shí)例沒(méi)有創(chuàng)建;如果一時(shí)間有多個(gè)線程同時(shí)調(diào)用LazySingleton.getLazyInstance()方法很有可能會(huì)產(chǎn)生多個(gè)實(shí)例。
例子:
publicclassSingletonClass{ //私有構(gòu)造函數(shù),保證類不能通過(guò)new創(chuàng)建 privateSingletonClass(){} privatestaticSingletonClassinstance=null; publicstaticSingletonClassgetInstance(){ if(instance==null){ //創(chuàng)建本類對(duì)象 instance=newSingletonClass(); } returninstance; } }
餓漢式:
應(yīng)用剛啟動(dòng)的時(shí)候,不管外部有沒(méi)有調(diào)用該類的實(shí)例方法,該類的實(shí)例就已經(jīng)創(chuàng)建好了。(空間換時(shí)間。)
優(yōu)點(diǎn):寫(xiě)法簡(jiǎn)單,在多線程下也能保證單例實(shí)例的唯一性,不用同步,運(yùn)行效率高。
缺點(diǎn):在外部沒(méi)有使用到該類的時(shí)候,該類的實(shí)例就創(chuàng)建了,若該類實(shí)例的創(chuàng)建比較消耗系統(tǒng)資源,并且外部一直沒(méi)有調(diào)用該實(shí)例,那么這部分的系統(tǒng)資源的消耗是沒(méi)有意義的。
例子:
publicclassSingleton{ //首先自己在內(nèi)部定義自己的一個(gè)實(shí)例,只供內(nèi)部調(diào)用 privatestaticfinalSingletoninstance=newSingleton(); //私有構(gòu)造函數(shù) privateSingleton(){ } //提供了靜態(tài)方法,外部可以直接調(diào)用 publicstaticSingletongetInstance(){ returninstance; } } 下面模擬單例模式在多線程下會(huì)出現(xiàn)的問(wèn)題 /** *懶漢式單例類 */ publicclassLazySingleton{ //為了易于模擬多線程下,懶漢式出現(xiàn)的問(wèn)題,我們?cè)趧?chuàng)建實(shí)例的構(gòu)造函數(shù)里面使當(dāng)前線程暫停了50毫秒 privateLazySingleton(){ try{ Thread.sleep(50); }catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println("生成LazySingleton實(shí)例一次!"); } privatestaticLazySingletonlazyInstance=null; publicstaticLazySingletongetLazyInstance(){ if(lazyInstance==null){ lazyInstance=newLazySingleton(); } returnlazyInstance; } }
測(cè)試代碼:我們?cè)跍y(cè)試代碼里面新建了10個(gè)線程,讓這10個(gè)線程同時(shí)調(diào)用LazySingleton.getLazyInstance()方法
publicclassSingletonTest{ publicstaticvoidmain(String[]args){ //創(chuàng)建十個(gè)線程調(diào) for(inti=0;i<10;i++){ newThread(){ @Override publicvoidrun(){ LazySingleton.getLazyInstance(); } }.start(); } } }
結(jié)果:
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
生成LazySingleton實(shí)例一次!
可以看出單例模式懶漢式在多線程的并發(fā)下也會(huì)出現(xiàn)問(wèn)題,
分析一下:多個(gè)線程同時(shí)訪問(wèn)上面的懶漢式單例,現(xiàn)在有兩個(gè)線程A和B同時(shí)訪問(wèn)LazySingleton.getLazyInstance()方法。
假設(shè)A先得到CPU的時(shí)間切片,A執(zhí)行到if(lazyInstance==null)時(shí),由于lazyInstance之前并沒(méi)有實(shí)例化,所以lazyInstance==null為true,在還沒(méi)有執(zhí)行實(shí)例創(chuàng)建的時(shí)候
此時(shí)CPU將執(zhí)行時(shí)間分給了線程B,線程B執(zhí)行到if(lazyInstance==null)時(shí),由于lazyInstance之前并沒(méi)有實(shí)例化,所以lazyInstance==null為true,線程B繼續(xù)往下執(zhí)行實(shí)例的創(chuàng)建過(guò)程,線程B創(chuàng)建完實(shí)例之后,返回。
此時(shí)CPU將時(shí)間切片分給線程A,線程A接著開(kāi)始執(zhí)行實(shí)例的創(chuàng)建,實(shí)例創(chuàng)建完之后便返回。由此看線程A和線程B分別創(chuàng)建了一個(gè)實(shí)例(存在2個(gè)實(shí)例了),這就導(dǎo)致了單例的失效。
解決辦法:我們可以在getLazyInstance方法上加上synchronized使其同步,但是這樣一來(lái),會(huì)降低整個(gè)訪問(wèn)的速度,而且每次都要判斷。
那么有沒(méi)有更好的方式來(lái)實(shí)現(xiàn)呢?我們可以考慮使用"雙重檢查加鎖"的方式來(lái)實(shí)現(xiàn),就可以既實(shí)現(xiàn)線程安全,又能夠使性能不受到很大的影響。我們看看具體解決代碼
publicclassLazySingleton{ privateLazySingleton(){ try{ Thread.sleep(50); }catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println("生成LazySingleton實(shí)例一次!"); } privatestaticLazySingletonlazyInstance=null; publicstaticLazySingletongetLazyInstance(){ //先檢查實(shí)例是否存在,如果不存在才進(jìn)入下面的同步塊 if(lazyInstance==null){ //同步塊,線程安全地創(chuàng)建實(shí)例 synchronized(LazySingleton.class){ //再次檢查實(shí)例是否存在,如果不存在才真正地創(chuàng)建實(shí)例 if(lazyInstance==null){ lazyInstance=newLazySingleton(); } } } returnlazyInstance; } }
這樣我們就可以在多線程并發(fā)下安全應(yīng)用單例模式中的懶漢模式。這種方法在代碼上可能就不怎么美觀,我們可以優(yōu)雅的使用一個(gè)內(nèi)部類來(lái)維護(hù)單例類的實(shí)例,下面看看代碼
publicclassGracefulSingleton{ privateGracefulSingleton(){ System.out.println("創(chuàng)建GracefulSingleton實(shí)例一次!"); } //類級(jí)的內(nèi)部類,也就是靜態(tài)的成員式內(nèi)部類,該內(nèi)部類的實(shí)例與外部類的實(shí)例沒(méi)有綁定關(guān)系,而且只有被調(diào)用到才會(huì)裝載,從而實(shí)現(xiàn)了延遲加載 privatestaticclassSingletonHoder{ //靜態(tài)初始化器,由JVM來(lái)保證線程安全 privatestaticGracefulSingletoninstance=newGracefulSingleton(); } publicstaticGracefulSingletongetInstance(){ returnSingletonHoder.instance; } }
說(shuō)一下我在實(shí)際開(kāi)發(fā)中的場(chǎng)景:為了程序的高效率使用多線程并發(fā),然而是循環(huán)調(diào)用,可能導(dǎo)致創(chuàng)建線程數(shù)過(guò)多,考慮采用線程池管理,這時(shí)候創(chuàng)建線程池仍然是處于循環(huán)調(diào)用中,也可能導(dǎo)致多個(gè)線程池,這時(shí)候就考慮使用單例模式。
源代碼:
publicclassThreadPoolFactoryUtil{ privateExecutorServiceexecutorService; //在構(gòu)造函數(shù)中創(chuàng)建線程池 privateThreadPoolFactoryUtil(){ //獲取系統(tǒng)處理器個(gè)數(shù),作為線程池?cái)?shù)量 intnThreads=Runtime.getRuntime().availableProcessors(); executorService=Executors.newFixedThreadPool(nThreads); } //定義一個(gè)靜態(tài)內(nèi)部類,內(nèi)部定義靜態(tài)成員創(chuàng)建外部類實(shí)例 privatestaticclassSingletonContainer{ privatestaticThreadPoolFactoryUtilutil=newThreadPoolFactoryUtil(); } //獲取本類對(duì)象 publicstaticThreadPoolFactoryUtilgetUtil(){ returnSingletonContainer.util; } publicExecutorServicegetExecutorService(){ returnexecutorService; } }
涉及到一個(gè)靜態(tài)內(nèi)部類,我們看看靜態(tài)內(nèi)部類的特點(diǎn):
1、靜態(tài)內(nèi)部類無(wú)需依賴于外部類,它可以獨(dú)立于外部對(duì)象而存在。
2、靜態(tài)內(nèi)部類,多個(gè)外部類的對(duì)象可以共享同一個(gè)內(nèi)部類的對(duì)象。
3、使用靜態(tài)內(nèi)部類的好處是加強(qiáng)了代碼的封裝性以及提高了代碼的可讀性。
4、普通內(nèi)部類不能聲明static的方法和變量,注意這里說(shuō)的是變量,常量(也就是finalstatic修飾的屬性)還是可以的,而靜態(tài)內(nèi)部類形似外部類,沒(méi)有任何限制。可以直接被用外部類名+內(nèi)部類名獲得。
以上是我在實(shí)際開(kāi)發(fā)中遇到的一些問(wèn)題,部分摘自網(wǎng)上代碼,結(jié)合實(shí)開(kāi)發(fā)際案例。如有不妥,希望大家及時(shí)指出!
相關(guān)文章
使用Springboot整合GridFS實(shí)現(xiàn)文件操作
這篇文章主要介紹了使用Springboot整合GridFS實(shí)現(xiàn)文件操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Spring框架構(gòu)造注入type屬性實(shí)例詳解
這篇文章主要介紹了Spring框架構(gòu)造注入type屬性實(shí)例詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12java ThreadLocal線程局部變量常用方法使用場(chǎng)景示例詳解
這篇文章主要介紹了為大家java ThreadLocal線程局部變量常用方法使用場(chǎng)景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07從千千靜聽(tīng)歌詞服務(wù)器獲取lrc歌詞示例分享
這篇文章主要介紹了使用PHP從千千靜聽(tīng)歌詞服務(wù)器獲取lrc歌詞的方法,大家參考使用吧2014-01-01Java中使用RediSearch實(shí)現(xiàn)高效的數(shù)據(jù)檢索功能
RediSearch是一款構(gòu)建在Redis上的搜索引擎,它為Redis數(shù)據(jù)庫(kù)提供了全文搜索、排序、過(guò)濾和聚合等高級(jí)查詢功能,本文將介紹如何在Java應(yīng)用中集成并使用RediSearch,以實(shí)現(xiàn)高效的數(shù)據(jù)檢索功能,感興趣的朋友跟著小編一起來(lái)看看吧2024-05-05Spring ApplicationListener源碼解析
這篇文章主要為大家介紹了Spring ApplicationListener源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Java實(shí)現(xiàn)PDF導(dǎo)出功能的示例代碼
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)PDF導(dǎo)出功能的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解下2023-09-09java開(kāi)發(fā)AOP面向切面編程入門(mén)
這篇文章主要介紹了java開(kāi)發(fā)的AOP面向切面編程入門(mén)的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步學(xué)有所得2021-10-10