Java設(shè)計模式基礎(chǔ) - 單例模式
單例模式是一種常見的設(shè)計模式,在這個模式下,單例對象的類必須保證只有一個實例存在,并提供返回實例對象的方法。在日常工作中,線程池、緩存、日志等對象通常被設(shè)計成單例模式,一方面減少了頻繁創(chuàng)建銷毀對象用以提升性能,另一方面避免了對共享資源的多重占用并簡化了訪問。
那么在高并發(fā)、多線程的環(huán)境下,是如何確保多個線程操作的是同一對象,也就是說保證對象的唯一性呢?這時就要用到單例模式,來確保實例化過程中,對象只被實例化了一次。本文將介紹一下單例模式的幾種實現(xiàn)方式及性能分析。
1.餓漢模式
餓漢模式比較簡單,在實例初始化的時候不管有沒有用到,都會把實例先創(chuàng)建好,等待被調(diào)用。
public class HungrySingleton { private static HungrySingleton instance=new HungrySingleton(); private HungrySingleton(){} //返回實例對象 public static HungrySingleton getInstance(){ return instance; } }
由于在加載的時候已經(jīng)被實例化,只會創(chuàng)建一個實例,因此餓漢模式是線程安全的,能夠充分保證單例。但是沒有實現(xiàn)延遲加載,可能很長時間不被使用,影響程序性能。
2.懶漢模式
懶漢模式就是實例在被用到的時候才去創(chuàng)建,在使用的同時去檢查有沒有實例,如果有則返回,沒有則新建。
public class HoonSingleton { private static HoonSingleton instance = null; public HoonSingleton() { } public HoonSingleton getInstance() { if (instance == null) { instance = new HoonSingleton(); } return instance; } }
可以看出,在懶漢模式中,單例實例會被延遲加載,即只有在真正使用的時候才會實例化一個對象并交給自己的引用。由于使用了懶加載,因此在性能上要優(yōu)于餓漢模式。
但是在多線程環(huán)境下,這種方法并不能夠保證實例對象的唯一性,多線程時可能多個線程同時去實例化對象,因此不能保證線程的安全性。在此基礎(chǔ)上進行改進,通過在getInstance()方法上加synchronized關(guān)鍵字,實現(xiàn)同步,可以實現(xiàn)線程安全。
public synchronized static HoonSingleton getInstance() { if (instance == null) { instance = new HoonSingleton(); } return instance; }
通過使用synchronized保證了對臨界資源的同步互斥訪問,也就保證了單例同步方法,這一方式實現(xiàn)了線程安全,但是相應(yīng)的該方法退化到了串行執(zhí)行,并且同步方法的作用域比較大,鎖的粒度太大,一定程度上降低了程序運行效率。
3.DCL模式
DCL模式又稱為雙檢鎖(Double Check Locking),也叫雙重校驗鎖,綜合了懶漢式和餓漢式兩者的優(yōu)點整合而成。
public class DCL { private static DCL instance=null; private DCL(){ } public static DCL getInstance(){ if(null==instance) synchronized (DCL.class){ if(null==instance) instance=new DCL(); } return instance; } }
DCL中,在synchronized關(guān)鍵字內(nèi)外都加了一層 if 條件判斷,這樣既保證了線程安全,又比直接上鎖提高了執(zhí)行效率,還節(jié)省了內(nèi)存空間。因此,在實現(xiàn)了懶加載與保證線程安全性的同時,也保證了較好的性能。
盡管DCL看起來已經(jīng)非常完善了,但是由于存在JVM指令重排序的存在(不清楚的可以查看上一篇文章),使得DCL仍然存在一些問題。
instance=new DCL();
盡管是很簡單的一個語句,但是從執(zhí)行上來看,這并不是一個原子操作。這一語句大概完成了三件事情:
給instance實例分配內(nèi)存
使用instance的構(gòu)造方法實例對象
將instance對象指向分配的內(nèi)存空間,必須注意,到此為止instance返回就已經(jīng)是非null的對象了
在此情況下,JVM為了優(yōu)化指令提高程序運行效率,可能會將執(zhí)行順序中的第2、3步顛倒一下。以2個線程為例,可能出現(xiàn)以下情況:
線程1,發(fā)現(xiàn)對象未實例化,準(zhǔn)備開始執(zhí)行構(gòu)造方法實例對象;
線程2調(diào)用instance實例,發(fā)現(xiàn)對象已經(jīng)不為null,直接返回對象;
對象構(gòu)造方法未執(zhí)行完畢,線程2調(diào)用instance中的一些對象返回空指針異常。
根據(jù)以上分析可知,解決這個問題可以通過加volatile關(guān)鍵字來確定指令執(zhí)行順序,避免指令重排序
private volatile static DCL instance=null;
4.Holder模式
Holder模式也被稱為靜態(tài)內(nèi)部類模式,在該模式下,可以通過使用內(nèi)部靜態(tài)類來以懶漢模式的思想來實現(xiàn)線程安全的對象單例。
public class HolderDemo { private HolderDemo() {} private static class Holder { private static HolderDemo instance = new HolderDemo(); } public static HolderDemo getInstance() { return Holder.instance; } }
可以看出,在聲明類的時候,它的成員中不包含需要聲明的實例變量,而放到它的內(nèi)部靜態(tài)類中去創(chuàng)建實例。而靜態(tài)的成員式內(nèi)部類,該內(nèi)部類的實例與外部類的實例沒有綁定關(guān)系,只有被調(diào)用到時才會裝載,這樣一來也實現(xiàn)了懶加載。
5.枚舉方式
枚舉實現(xiàn)方式是在Effective Java一書中被提到的,具有功能完善使用簡單,無償?shù)靥峁┝诵蛄谢瘷C制,在面對復(fù)雜的序列化或者反射攻擊時仍然可以絕對防止多次實例化等優(yōu)點。
public class EnumSingletonDemo { private EnumSingletonDemo() { } private enum EnumHolder { INSTANCE; private EnumSingletonDemo instance; EnumHolder(){ instance = new EnumSingletonDemo(); } } public static EnumSingletonDemo getInstance() { return EnumHolder.INSTANCE.instance; } }
由于Java中規(guī)定了每個枚舉類型及其定義的枚舉變量在JVM中都是唯一的,所以在加載的過程中只能被實例化一次,所以在其初始化的過程中是線程安全的。
在序列化方面,Java中枚舉的序列化和反序列化都做了特殊的規(guī)定,這就可以避免反序列化過程中由于反射而導(dǎo)致的單例被破壞問題。使用枚舉的方式,能夠有效防止使用反射強行調(diào)用構(gòu)造方法創(chuàng)建實例。
總結(jié)
本文介紹了單例模式的主要思想,并列舉出了它的幾種經(jīng)典實現(xiàn),并對幾種實現(xiàn)的線程安全性與執(zhí)行效率進行了分析。總的來說,可以按照以下規(guī)則進行實現(xiàn)方式的選擇:
減少使用懶漢模式,線程安全或不安全模式下均有一定缺陷
如果設(shè)計序列化與反序列化時,可以選擇枚舉的方式
如果要實現(xiàn)懶加載,可以使用DCL及Holder模式
未聲明需要懶加載,可以選擇餓漢模式
最后
覺得對您有所幫助,小伙伴們可以點個贊啊,非常感謝~
公眾號『碼農(nóng)參上』,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術(shù)。歡迎來加Hydra好友 (- DrHydra9),圍觀朋友圈,做個之交啊。
Java 多線程
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。