漫談設(shè)計(jì)模式-技術(shù)要點(diǎn)詳解
第3章 單例(Singleton)模式
3.1 概述
如果要保證系統(tǒng)里一個(gè)類最多只能存在一個(gè)實(shí)例時(shí),我們就需要單例模式。這種情況在我們應(yīng)用中經(jīng)常碰到,例如緩存池,數(shù)據(jù)庫(kù)連接池,線程池,一些應(yīng)用服務(wù)實(shí)例等。在多線程環(huán)境中,為了保證實(shí)例的唯一性其實(shí)并不簡(jiǎn)單,這章將和讀者一起探討如何實(shí)現(xiàn)單例模式。
3.2 最簡(jiǎn)單的單例
為了限制該類的對(duì)象被隨意地創(chuàng)建,我們保證該類構(gòu)造方法是私有的,這樣外部類就無(wú)法創(chuàng)建該類型的對(duì)象了;另外,為了給客戶對(duì)象提供對(duì)此單例對(duì)象的使用,我們?yōu)樗峁┮粋€(gè)全局訪問(wèn)點(diǎn),代碼如下所示:
- public class Singleton {
- private static Singleton instance = new Singleton();
- //other fields…
- private Singleton() {
- }
- public static Singleton getInstance() {
- return instance;
- }
- //other methods…
- }
代碼注解:
l Singleton類的只有一個(gè)構(gòu)造方法,它是被private修飾的,客戶對(duì)象無(wú)法創(chuàng)建該類實(shí)例。
l 我們?yōu)榇藛卫龑?shí)現(xiàn)的全局訪問(wèn)點(diǎn)是public static Singleton getInstance()方法,注意,instance變量是私有的,外界無(wú)法訪問(wèn)的。
讀者還可以定義instance變量是public的,這樣把屬性直接暴露給其他對(duì)象,就沒(méi)必要實(shí)現(xiàn)public static Singleton getInstance()方法,但是可讀性沒(méi)有方法來(lái)的直接,而且把該實(shí)例變量的名字直接暴露給客戶程序,增加了代碼的耦合度,如果改變此變量名稱,會(huì)引起客戶類的改變。
還有一點(diǎn),如果該實(shí)例需要比較復(fù)雜的初始化過(guò)程時(shí),把這個(gè)過(guò)程應(yīng)該寫(xiě)在static{…}代碼塊中。
l 此實(shí)現(xiàn)是線程安全的,當(dāng)多個(gè)線程同時(shí)去訪問(wèn)該類的getInstance()方法時(shí),不會(huì)初始化多個(gè)不同的對(duì)象,這是因?yàn)?,JVM(Java Virtual Machine)在加載此類時(shí),對(duì)于static屬性的初始化只能由一個(gè)線程執(zhí)行且僅一次[1]。
由于此單例提供了靜態(tài)的公有方法,那么客戶使用單例模式的代碼也就非常簡(jiǎn)單了,如下所示:
Singleton singleton = Singleton.getInstance();
3.3 進(jìn)階
3.3.1 延遲創(chuàng)建
如果出于性能等的考慮,我們希望延遲實(shí)例化單例對(duì)象(Static屬性在加載類是就會(huì)被初始化),只有在第一次使用該類的實(shí)例時(shí)才去實(shí)例化,我們應(yīng)該怎么辦呢?
這個(gè)其實(shí)并不難做到,我們把單例的實(shí)例化過(guò)程移至getInstance()方法,而不在加載類時(shí)預(yù)先創(chuàng)建。當(dāng)訪問(wèn)此方法時(shí),首先判斷該實(shí)例是不是已經(jīng)被實(shí)例化過(guò)了,如果已被初始化,則直接返回這個(gè)對(duì)象的引用;否則,創(chuàng)建這個(gè)實(shí)例并初始化,最后返回這個(gè)對(duì)象引用。代碼片段如下所示:
- public class UnThreadSafeSingelton {
- //variables and constructors…
- public static UnThreadSafeSingelton getInstance() {
- if(instatnce ==null){
- instatnce = new UnThreadSafeSingelton();
- }
- return instatnce;
- }
- }
我們使用這句if(instatnce ==null) 判斷是否實(shí)例化完成了。此方法不是線程安全的,接下來(lái)我們將會(huì)討論。
3.3.2 線程安全
上節(jié)我們創(chuàng)建了可延遲初始化的單例,然而不幸的是,在高并發(fā)的環(huán)境中,getInstance()方法返回了多個(gè)指向不同的該類實(shí)例,究竟是什么原因呢?我們針對(duì)此方法,給出兩個(gè)線程并發(fā)訪問(wèn)getInstance()方法時(shí)的一種情況,如下所示:
t1 t2
1 if(instatnce ==null)
2 if(instatnce ==null)
3 instatnce = new UnThreadSafeSingelton();
4 return instatnce;
5 instatnce = new UnThreadSafeSingelton()
6 return instatnce;
如果這兩個(gè)線程按照上述步驟執(zhí)行,不難發(fā)現(xiàn),在時(shí)刻1和2,由于還沒(méi)有創(chuàng)建單例對(duì)象,Thread1和Thread2都會(huì)進(jìn)入創(chuàng)建單例實(shí)例的代碼塊分別創(chuàng)建實(shí)例。在時(shí)刻3,Thread1創(chuàng)建了一個(gè)實(shí)例對(duì)象,但是Thread2此時(shí)已無(wú)法知道,繼續(xù)創(chuàng)建一個(gè)新的實(shí)例對(duì)象,于是這兩個(gè)線程持有的實(shí)例并非為同一個(gè)。更為糟糕的是,在沒(méi)有自動(dòng)內(nèi)存回收機(jī)制的語(yǔ)言平臺(tái)上運(yùn)行這樣的單例模式,例如使用C++編寫(xiě)此模式,因?yàn)槲覀冋J(rèn)為創(chuàng)建了一個(gè)單例實(shí)例,忽略了其他線程所產(chǎn)生的對(duì)象,不會(huì)手動(dòng)去回收它們,引起了內(nèi)存泄露。
為了解決這個(gè)問(wèn)題,我們給此方法添加synchronized關(guān)鍵字,代碼如下:
- public class ThreadSafeSingelton {
- //variables and constructors…
- public static synchronized ThreadSafeSingelton getInstance() {
- if(instatnce ==null){
- instatnce = new ThreadSafeSingelton();
- }
- return instatnce;
- }
- }
這樣,再多的線程訪問(wèn)都只會(huì)實(shí)例化一個(gè)單例對(duì)象。
3.3.3 Double-Check Locking
上述途徑雖然實(shí)現(xiàn)了多線程的安全訪問(wèn),但是在多線程高并發(fā)訪問(wèn)的情況下,給此方法加上synchronized關(guān)鍵字會(huì)使得性能大不如前。我們仔細(xì)分析一下不難發(fā)現(xiàn),使用了synchronized關(guān)鍵字對(duì)整個(gè)getInstance()方法進(jìn)行同步是沒(méi)有必要的:我們只要保證實(shí)例化這個(gè)對(duì)象的那段邏輯被一個(gè)線程執(zhí)行就可以了,而返回引用的那段代碼是沒(méi)有必要同步的。按照這個(gè)想法,我們的代碼片段大致如下所示:
- public class DoubleCheckSingleton {
- private volatile static DoubleCheckSingleton instatnce = null;
- //constructors
- public static DoubleCheckSingleton getInstance() {
- if (instatnce == null) { //check if it is created.
- synchronized (DoubleCheckSingleton.class) { //synchronize creation block
- if (instatnce == null) //double check if it is created
- instatnce = new DoubleCheckSingleton();
- }
- }
- return instatnce;
- }
- }
代碼注解:
l 在getInstance()方法里,我們首先判斷此實(shí)例是否已經(jīng)被創(chuàng)建了,如果還沒(méi)有創(chuàng)建,首先使用synchronized同步實(shí)例化代碼塊。在同步代碼塊里,我們還需要再次檢查是否已經(jīng)創(chuàng)建了此類的實(shí)例,這是因?yàn)椋喝绻麤](méi)有第二次檢查,這時(shí)有兩個(gè)線程Thread A和Thread B同時(shí)進(jìn)入該方法,它們都檢測(cè)到instatnce為null,不管哪一個(gè)線程先占據(jù)同步鎖創(chuàng)建實(shí)例對(duì)象,都不會(huì)阻止另外一個(gè)線程繼續(xù)進(jìn)入實(shí)例化代碼塊重新創(chuàng)建實(shí)例對(duì)象,這樣,同樣會(huì)生成兩個(gè)實(shí)例對(duì)象。所以,我們?cè)谕降拇a塊里,進(jìn)行第二次判斷判斷該對(duì)象是否已被創(chuàng)建。
正是由于使用了兩次的檢查,我們稱之為double-checked locking模式。
l 屬性instatnce是被volatile修飾的,因?yàn)関olatile具有synchronized的可見(jiàn)性特點(diǎn),也就是說(shuō)線程能夠自動(dòng)發(fā)現(xiàn)volatile變量的最新值。這樣,如果instatnce實(shí)例化成功,其他線程便能立即發(fā)現(xiàn)。
注意:
此程序只有在JAVA 5及以上版本才能正常運(yùn)行,在以前版本不能保證其正常運(yùn)行。這是由于Java平臺(tái)的內(nèi)存模式容許out-of-order writes引起的,假定有兩個(gè)線程,Thread 1和Thread 2,它們執(zhí)行以下步驟:
1. Thread 1發(fā)現(xiàn)instatnce沒(méi)有被實(shí)例化,它獲得鎖并去實(shí)例化此對(duì)象,JVM容許在沒(méi)有完全實(shí)例化完成時(shí),instance變量就指向此實(shí)例,因?yàn)檫@些步驟可以是out-of-order writes的,此時(shí)instance==null為false,之前的版本即使用volatile關(guān)鍵字修飾也無(wú)效。
2. 在初始化完成之前,Thread 2進(jìn)入此方法,發(fā)現(xiàn)instance已經(jīng)不為null了,Thread 2便認(rèn)為該實(shí)例初始化完成了,使用這個(gè)未完全初始化的實(shí)例對(duì)象,則很可能引起系統(tǒng)的崩潰。
3.3.4 Initialization on demand holder
要使用線程安全的延遲的單例初始化,我們還有一種方法,稱為Initialization on demand holder模式,代碼如下所示:
- public class LazyLoadedSingleton {
- private LazyLoadedSingleton() {
- }
- private static class LazyHolder { //holds the singleton class
- private static final LazyLoadedSingleton singletonInstatnce = new LazyLoadedSingleton();
- }
- public static LazyLoadedSingleton getInstance() {
- return LazyHolder.singletonInstatnce;
- }
- }
當(dāng)JVM加載LazyLoadedSingleton類時(shí),由于該類沒(méi)有static屬性,所以加載完成后便即可返回。只有第一次調(diào)用getInstance()方法時(shí),JVM才會(huì)加載LazyHolder類,由于它包含一個(gè)static屬性singletonInstatnce,所以會(huì)首先初始化這個(gè)變量,根據(jù)前面的介紹,我們知道此過(guò)程并不會(huì)出現(xiàn)并發(fā)問(wèn)題(JLS保證),這樣即實(shí)現(xiàn)了一個(gè)既線程安全又支持延遲加載的單例模式。
3.3.5 Singleton的序列化
如果單例類實(shí)現(xiàn)了Serializable接口,這時(shí)我們得特別注意,因?yàn)槲覀冎涝谀J(rèn)情況下,每次反序列化(Desierialization)總會(huì)創(chuàng)建一個(gè)新的實(shí)例對(duì)象,這樣一個(gè)系統(tǒng)會(huì)出現(xiàn)多個(gè)對(duì)象供使用。我們應(yīng)該怎么辦呢?
熟悉Java序列化的讀者可能知道,我們需要在readResolve()方法里做文章,此方法在反序列化完成之前被執(zhí)行,我們?cè)诖朔椒ɡ锾鎿Q掉反序列化出來(lái)的那個(gè)新的實(shí)例,讓其指向內(nèi)存中的那個(gè)單例對(duì)象即可,代碼實(shí)現(xiàn)如下:
- import java.io.Serializable;
- public class SerialibleSingleton implements Serializable {
- private static final long serialVersionUID = -6099617126325157499L;
- static SerialibleSingleton singleton = new SerialibleSingleton();
- private SerialibleSingleton() {
- }
- // This method is called immediately after an object of this class is deserialized.
- // This method returns the singleton instance.
- private Object readResolve() {
- return singleton;
- }
- }
方法readResolve()直接返回singleton單例,這樣,我們?cè)趦?nèi)存中始終保持了一個(gè)唯一的單例對(duì)象。
3.4 總結(jié)
通過(guò)這一章的學(xué)習(xí),我相信大家對(duì)于基本的單例模式已經(jīng)有了一個(gè)比較充分的認(rèn)識(shí)。其實(shí)我們這章討論的是在同一個(gè)JVM中,如何保證一個(gè)類只有一個(gè)單例,如果在分布式環(huán)境中,我們可能需要考慮如何保證在整個(gè)應(yīng)用(可能分布在不同JVM上)只有一個(gè)實(shí)例,但這也超出本書(shū)范疇,在這里將不再做深入研究,有興趣的讀者可以查閱相關(guān)資料深入研究。
________________________________________
[1] Static屬性和Static初始化塊(Static Initializers)的初始化過(guò)程是串行的,這個(gè)由JLS(Java Language Specification)保證,參見(jiàn)James Gosling, Bill Joy, Guy Steele and Gilad Bracha編寫(xiě)的《 The Java™ Language Specification Third Edition》一書(shū)的12.4一節(jié)。
原文鏈接:http://redhat.iteye.com/blog/1007884