多線程編程Java設(shè)計(jì)模式JVM Abstract 在開(kāi)發(fā)中,,如果某個(gè)實(shí)例的創(chuàng)建需要消耗很多系統(tǒng)資源,,那么我們通常會(huì)使用惰性加載機(jī)制,也就是說(shuō)只有當(dāng)使用到這個(gè)實(shí)例的時(shí)候才會(huì)創(chuàng)建這個(gè)實(shí)例,,這個(gè)好處在單例模式中得到了廣泛應(yīng)用,。這個(gè)機(jī)制在single-threaded環(huán)境下的實(shí)現(xiàn)非常簡(jiǎn)單,然而在multi-threaded環(huán)境下卻存在隱患。本文重點(diǎn)介紹惰性加載機(jī)制以及其在多線程環(huán)境下的使用方法,。(作者numberzero,,參考IBM文章《Double-checked locking and the Singleton pattern》,歡迎轉(zhuǎn)載與討論) 1 單例模式的惰性加載 通常當(dāng)我們?cè)O(shè)計(jì)一個(gè)單例類(lèi)的時(shí)候,,會(huì)在類(lèi)的內(nèi)部構(gòu)造這個(gè)類(lèi)(通過(guò)構(gòu)造函數(shù),,或者在定義處直接創(chuàng)建),并對(duì)外提供一個(gè)static getInstance方法提供獲取該單例對(duì)象的途徑,。例如: Java代碼 這樣的代碼缺點(diǎn)是:第一次加載類(lèi)的時(shí)候會(huì)連帶著創(chuàng)建Singleton實(shí)例,,這樣的結(jié)果與我們所期望的不同,因?yàn)閯?chuàng)建實(shí)例的時(shí)候可能并不是我們需要這個(gè)實(shí)例的時(shí)候,。同時(shí)如果這個(gè)Singleton實(shí)例的創(chuàng)建非常消耗系統(tǒng)資源,,而應(yīng)用始終都沒(méi)有使用Singleton實(shí)例,那么創(chuàng)建Singleton消耗的系統(tǒng)資源就被白白浪費(fèi)了,。 為了避免這種情況,,我們通常使用惰性加載的機(jī)制,也就是在使用的時(shí)候才去創(chuàng)建,。以上代碼的惰性加載代碼如下: Java代碼 這樣,,當(dāng)我們第一次調(diào)用Singleton.getInstance()的時(shí)候,這個(gè)單例才被創(chuàng)建,,而以后再次調(diào)用的時(shí)候僅僅返回這個(gè)單例就可以了,。 2 惰性加載在多線程中的問(wèn)題 先將惰性加載的代碼提取出來(lái): Java代碼 這是如果兩個(gè)線程A和B同時(shí)執(zhí)行了該方法,然后以如下方式執(zhí)行:
2. B進(jìn)入if判斷,此時(shí)A還沒(méi)有創(chuàng)建foo,,因此foo也為null,,因此B也進(jìn)入if內(nèi) 3. A創(chuàng)建了一個(gè)Foo并返回 4. B也創(chuàng)建了一個(gè)Foo并返回 此時(shí)問(wèn)題出現(xiàn)了,我們的單例被創(chuàng)建了兩次,,而這并不是我們所期望的,。 3 各種解決方案及其存在的問(wèn)題 3.1 使用Class鎖機(jī)制 以上問(wèn)題最直觀的解決辦法就是給getInstance方法加上一個(gè)synchronize前綴,這樣每次只允許一個(gè)現(xiàn)成調(diào)用getInstance方法: Java代碼 這種解決辦法的確可以防止錯(cuò)誤的出現(xiàn),,但是它卻很影響性能:每次調(diào)用getInstance方法的時(shí)候都必須獲得Singleton的鎖,,而實(shí)際上,當(dāng)單例實(shí)例被創(chuàng)建以后,,其后的請(qǐng)求沒(méi)有必要再使用互斥機(jī)制了 3.2 double-checked locking 曾經(jīng)有人為了解決以上問(wèn)題,,提出了double-checked locking的解決方案 Java代碼 讓我們來(lái)看一下這個(gè)代碼是如何工作的:首先當(dāng)一個(gè)線程發(fā)出請(qǐng)求后,會(huì)先檢查instance是否為null,,如果不是則直接返回其內(nèi)容,,這樣避免了進(jìn)入synchronized塊所需要花費(fèi)的資源,。其次,即使第2節(jié)提到的情況發(fā)生了,,兩個(gè)線程同時(shí)進(jìn)入了第一個(gè)if判斷,,那么他們也必須按照順序執(zhí)行synchronized塊中的代碼,第一個(gè)進(jìn)入代碼塊的線程會(huì)創(chuàng)建一個(gè)新的Singleton實(shí)例,,而后續(xù)的線程則因?yàn)闊o(wú)法通過(guò)if判斷,,而不會(huì)創(chuàng)建多余的實(shí)例。 上述描述似乎已經(jīng)解決了我們面臨的所有問(wèn)題,,但實(shí)際上,,從JVM的角度講,這些代碼仍然可能發(fā)生錯(cuò)誤,。 對(duì)于JVM而言,,它執(zhí)行的是一個(gè)個(gè)Java指令。在Java指令中創(chuàng)建對(duì)象和賦值操作是分開(kāi)進(jìn)行的,,也就是說(shuō)instance = new Singleton();語(yǔ)句是分兩步執(zhí)行的,。但是JVM并不保證這兩個(gè)操作的先后順序,也就是說(shuō)有可能JVM會(huì)為新的Singleton實(shí)例分配空間,,然后直接賦值給instance成員,,然后再去初始化這個(gè)Singleton實(shí)例。這樣就使出錯(cuò)成為了可能,,我們?nèi)匀灰訟,、B兩個(gè)線程為例:
2. A首先進(jìn)入synchronized塊,,由于instance為null,,所以它執(zhí)行instance = new Singleton(); 3. 由于JVM內(nèi)部的優(yōu)化機(jī)制,JVM先畫(huà)出了一些分配給Singleton實(shí)例的空白內(nèi)存,,并賦值給instance成員(注意此時(shí)JVM沒(méi)有開(kāi)始初始化這個(gè)實(shí)例),,然后A離開(kāi)了synchronized塊。 4. B進(jìn)入synchronized塊,,由于instance此時(shí)不是null,,因此它馬上離開(kāi)了synchronized塊并將結(jié)果返回給調(diào)用該方法的程序,。 5. 此時(shí)B線程打算使用Singleton實(shí)例,,卻發(fā)現(xiàn)它沒(méi)有被初始化,于是錯(cuò)誤發(fā)生了,。 4 通過(guò)內(nèi)部類(lèi)實(shí)現(xiàn)多線程環(huán)境中的單例模式 為了實(shí)現(xiàn)慢加載,,并且不希望每次調(diào)用getInstance時(shí)都必須互斥執(zhí)行,最好并且最方便的解決辦法如下: Java代碼 JVM內(nèi)部的機(jī)制能夠保證當(dāng)一個(gè)類(lèi)被加載的時(shí)候,,這個(gè)類(lèi)的加載過(guò)程是線程互斥的,。這樣當(dāng)我們第一次調(diào)用getInstance的時(shí)候,,JVM能夠幫我們保證instance只被創(chuàng)建一次,并且會(huì)保證把賦值給instance的內(nèi)存初始化完畢,,這樣我們就不用擔(dān)心3.2中的問(wèn)題,。此外該方法也只會(huì)在第一次調(diào)用的時(shí)候使用互斥機(jī)制,這樣就解決了3.1中的低效問(wèn)題,。最后instance是在第一次加載SingletonContainer類(lèi)時(shí)被創(chuàng)建的,,而SingletonContainer類(lèi)則在調(diào)用getInstance方法的時(shí)候才會(huì)被加載,因此也實(shí)現(xiàn)了惰性加載,。
|
|