摘要:對(duì)于而言,它執(zhí)行的是一個(gè)個(gè)指令。在指令中創(chuàng)建對(duì)象和賦值操作是分開(kāi)進(jìn)行的,也就是說(shuō)語(yǔ)句是分兩步執(zhí)行的。此時(shí)線程打算使用實(shí)例,卻發(fā)現(xiàn)它沒(méi)有被初始化,于是錯(cuò)誤發(fā)生了。
1.餓漢式單例
public class Singleton { private static Singleton instance = new Singleton(); private Singleton(){ … } public static Singleton getInstance(){ return instance; } }
這樣的代碼缺點(diǎn)是:第一次加載類的時(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)建。以上代碼的惰性加載代碼如下:
2.懶漢式單例
public class Singleton{ private static Singleton instance = null; private Singleton(){ … } public static Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; } }
線程安全問(wèn)題
這是如果兩個(gè)線程A和B同時(shí)執(zhí)行了該方法,然后以如下方式執(zhí)行:
A進(jìn)入if判斷,此時(shí)instance為null,因此進(jìn)入if內(nèi)
B進(jìn)入if判斷,此時(shí)A還沒(méi)有創(chuàng)建instance,因此instance也為null,因此B也進(jìn)入if內(nèi)
A創(chuàng)建了一個(gè)instance并返回
B也創(chuàng)建了一個(gè)instance并返回
此時(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方法:
public static synchronized Singleton getInstance(){ if (instance == null) instance = new Singleton(); return instance; }
這種解決辦法的確可以防止錯(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的解決方案
public static Singleton getInstance(){ if (instance == null) synchronized(instance){ if(instance == null) instance = new Singleton(); } return instance; }
讓我們來(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í)例。(即先賦值指向了內(nèi)存地址,再初始化)這樣就使出錯(cuò)成為了可能,我們?nèi)匀灰訟、B兩個(gè)線程為例:
A、B線程同時(shí)進(jìn)入了第一個(gè)if判斷
A首先進(jìn)入synchronized塊,由于instance為null,所以它執(zhí)行instance = new Singleton();
由于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塊。
B進(jìn)入synchronized塊,由于instance此時(shí)不是null,因此它馬上離開(kāi)了synchronized塊并將結(jié)果返回給調(diào)用該方法的程序。
此時(shí)B線程打算使用Singleton實(shí)例,卻發(fā)現(xiàn)它沒(méi)有被初始化,于是錯(cuò)誤發(fā)生了。
4 通過(guò)內(nèi)部類實(shí)現(xiàn)多線程環(huán)境中的單例模式
為了實(shí)現(xiàn)慢加載,并且不希望每次調(diào)用getInstance時(shí)都必須互斥執(zhí)行,最好并且最方便的解決辦法如下:
public class Singleton{ private Singleton(){ … } private static class SingletonContainer{ private static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonContainer.instance; } }
JVM內(nèi)部的機(jī)制能夠保證當(dāng)一個(gè)類被加載的時(shí)候,這個(gè)類的加載過(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類時(shí)被創(chuàng)建的,而SingletonContainer類則在調(diào)用getInstance方法的時(shí)候才會(huì)被加載,因此也實(shí)現(xiàn)了惰性加載。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/73967.html
摘要:雙重檢查鎖定以下稱為已被廣泛當(dāng)做多線程環(huán)境下延遲初始化的一種高效手段。由于沒(méi)有對(duì)這些做出明確規(guī)定,很難說(shuō)是否有效。可以在中使用顯式的內(nèi)存屏障來(lái)使生效,但中并沒(méi)有這些屏障。如果改變鎖釋放的語(yǔ)義釋放時(shí)執(zhí)行一個(gè)雙向的內(nèi)存屏障將會(huì)帶來(lái)性能損失。 雙重檢查鎖定(以下稱為DCL)已被廣泛當(dāng)做多線程環(huán)境下延遲初始化的一種高效手段。 showImg(http://segmentfault.com/i...
摘要:關(guān)于對(duì)于重排序的講解,強(qiáng)烈推薦閱讀程曉明寫(xiě)的深入理解內(nèi)存模型二重排序。語(yǔ)義語(yǔ)義單線程下,為了優(yōu)化可以對(duì)操作進(jìn)行重排序。編譯器和處理器為單個(gè)線程實(shí)現(xiàn)了語(yǔ)義,但對(duì)于多線程并不實(shí)現(xiàn)語(yǔ)義。雙重加載的單例模式分析即雙重檢查加鎖。 版權(quán)聲明:本文由吳仙杰創(chuàng)作整理,轉(zhuǎn)載請(qǐng)注明出處:https://segmentfault.com/a/1190000009231182 1. 引言 在開(kāi)始分析雙重加鎖單...
摘要:總結(jié)我們主要介紹到了以下幾種方式實(shí)現(xiàn)單例模式餓漢方式線程安全懶漢式非線程安全和關(guān)鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設(shè)計(jì)模式中文版第二版設(shè)計(jì)模式深入理解單例模式我是一個(gè)以架構(gòu)師為年之內(nèi)目標(biāo)的小小白。 初遇設(shè)計(jì)模式在上個(gè)寒假,當(dāng)時(shí)把每個(gè)設(shè)計(jì)模式過(guò)了一遍,對(duì)設(shè)計(jì)模式有了一個(gè)最初級(jí)的了解。這個(gè)學(xué)期借了幾本設(shè)計(jì)模式的書(shū)籍看,聽(tīng)了老師的設(shè)計(jì)模式課,對(duì)設(shè)計(jì)模式算是有個(gè)更進(jìn)一步的認(rèn)識(shí)。...
摘要:基于的雙重檢查鎖定的解決方案對(duì)于前面的基于雙重檢查鎖定來(lái)實(shí)現(xiàn)延遲初始化的方案指示例代碼,我們只需要做一點(diǎn)小的修改把聲明為型,就可以實(shí)現(xiàn)線程安全的延遲初始化。 雙重檢查鎖定的由來(lái) 在java程序中,有時(shí)候可能需要推遲一些高開(kāi)銷的對(duì)象初始化操作,并且只有在使用這些對(duì)象時(shí)才進(jìn)行初始化。此時(shí)程序員可能會(huì)采用延遲初始化。但要正確實(shí)現(xiàn)線程安全的延遲初始化需要一些技巧,否則很容易出現(xiàn)問(wèn)題。比如,下...
摘要:在設(shè)計(jì)模式一書(shū)中,將單例模式稱作單件模式。通過(guò)關(guān)鍵字,來(lái)保證不會(huì)同時(shí)有兩個(gè)線程進(jìn)入該方法的實(shí)例對(duì)象改善多線程問(wèn)題為了符合大多數(shù)程序,很明顯地,我們需要確保單例模式能在多線程的情況下正常工作。 在《Head First 設(shè)計(jì)模式》一書(shū)中,將單例模式稱作單件模式。這里為了適應(yīng)大環(huán)境,把它稱之為大家更熟悉的單例模式。 一、了解單例模式 1.1 什么是單例模式 單例模式確保一個(gè)類只有一個(gè)實(shí)例,...
閱讀 2245·2023-04-26 00:23
閱讀 921·2021-09-08 09:45
閱讀 2510·2019-08-28 18:20
閱讀 2644·2019-08-26 13:51
閱讀 1674·2019-08-26 10:32
閱讀 1463·2019-08-26 10:24
閱讀 2099·2019-08-26 10:23
閱讀 2267·2019-08-23 18:10