深入理解TheadLocal

1.什么是TheadLocal

? ??早在JDK 1.2的版本中就提供java.lang.ThreadLocal庶柿,ThreadLocal為解決多線程程序的并發(fā)問(wèn)題提供了一種新的思路。使用這個(gè)工具類(lèi)可以很簡(jiǎn)潔地編寫(xiě)出優(yōu)美的多線程程序朱转。

當(dāng)使用ThreadLocal維護(hù)變量時(shí)谦炒,ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本去扣,而不會(huì)影響其它線程所對(duì)應(yīng)的副本柱衔。

從線程的角度看,目標(biāo)變量就象是線程的本地變量愉棱,這也是類(lèi)名中“Local”所要表達(dá)的意思唆铐。

所以,在Java中編寫(xiě)線程局部變量的代碼相對(duì)來(lái)說(shuō)要笨拙一些奔滑,因此造成線程局部變量沒(méi)有在Java開(kāi)發(fā)者中得到很好的普及艾岂。


2.ThreadLocal的原理

ThreadLocal,連接ThreadLocalMap和Thread朋其。來(lái)處理Thread的TheadLocalMap屬性王浴,包括init初始化屬性賦值脆炎、get對(duì)應(yīng)的變量,set設(shè)置變量等叼耙。通過(guò)當(dāng)前線程腕窥,獲取線程上的ThreadLocalMap屬性粒没,對(duì)數(shù)據(jù)進(jìn)行g(shù)et筛婉、set等操作。

ThreadLocalMap癞松,用來(lái)存儲(chǔ)數(shù)據(jù)爽撒,采用類(lèi)似hashmap機(jī)制,存儲(chǔ)了以threadLocal為key响蓉,需要隔離的數(shù)據(jù)為value的Entry鍵值對(duì)數(shù)組結(jié)構(gòu)硕勿。

ThreadLocal,有個(gè)ThreadLocalMap類(lèi)型的屬性枫甲,存儲(chǔ)的數(shù)據(jù)就放在這兒源武。



3.ThreadLocalMap和Thread,ThreadLocal 之間的關(guān)系

ThreadLocalMap是ThreadLocal內(nèi)部類(lèi),由ThreadLocal創(chuàng)建想幻,Thread有ThreadLocal.ThreadLocalMap類(lèi)型的屬性



ThreadLocalMap簡(jiǎn)介

看名字就知道是個(gè)map粱栖,沒(méi)錯(cuò),這就是個(gè)hashMap機(jī)制實(shí)現(xiàn)的map脏毯,用Entry數(shù)組來(lái)存儲(chǔ)鍵值對(duì)闹究,key是ThreadLocal對(duì)象,value則是具體的值食店。值得一提的是渣淤,為了方便GC,Entry繼承了WeakReference吉嫩,也就是弱引用价认。里面有一些具體關(guān)于如何清理過(guò)期的數(shù)據(jù)、擴(kuò)容等機(jī)制自娩,思路基本和hashmap差不多用踩,有興趣的可以自行閱讀了解,這邊只需知道大概的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)即可椒功。



Thread同步機(jī)制的比較

ThreadLocal和線程同步機(jī)制相比有什么優(yōu)勢(shì)呢捶箱?

Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離动漾。

在同步機(jī)制中丁屎,通過(guò)對(duì)象的鎖機(jī)制保證同一時(shí)間只有一個(gè)線程訪問(wèn)變量。這時(shí)該變量是多個(gè)線程共享的旱眯,使用同步機(jī)制要求程序慎密地分析什么時(shí)候?qū)ψ兞窟M(jìn)行讀寫(xiě)晨川,什么時(shí)候需要鎖定某個(gè)對(duì)象证九,什么時(shí)候釋放對(duì)象鎖等繁雜的問(wèn)題,程序設(shè)計(jì)和編寫(xiě)難度相對(duì)較大共虑。

而ThreadLocal則從另一個(gè)角度來(lái)解決多線程的并發(fā)訪問(wèn)愧怜。ThreadLocal會(huì)為每一個(gè)線程提供一個(gè)獨(dú)立的變量副本,從而隔離了多個(gè)線程對(duì)數(shù)據(jù)的訪問(wèn)沖突妈拌。因?yàn)槊恳粋€(gè)線程都擁有自己的變量副本拥坛,從而也就沒(méi)有必要對(duì)該變量進(jìn)行同步了。ThreadLocal提供了線程安全的共享對(duì)象尘分,在編寫(xiě)多線程代碼時(shí)猜惋,可以把不安全的變量封裝進(jìn)ThreadLocal。

概括起來(lái)說(shuō)培愁,對(duì)于多線程資源共享的問(wèn)題著摔,同步機(jī)制采用了“以時(shí)間換空間”的方式,而ThreadLocal采用了“以空間換時(shí)間”的方式定续。前者僅提供一份變量谍咆,讓不同的線程排隊(duì)訪問(wèn),而后者為每一個(gè)線程都提供了一份變量私股,因此可以同時(shí)訪問(wèn)而互不影響摹察。

Spring使用ThreadLocal解決線程安全問(wèn)題我們知道在一般情況下,只有無(wú)狀態(tài)的Bean才可以在多線程環(huán)境下共享庇茫,在Spring中港粱,絕大部分Bean都可以聲明為singleton作用域。就是因?yàn)镾pring對(duì)一些Bean(如RequestContextHolder旦签、TransactionSynchronizationManager查坪、LocaleContextHolder等)中非線程安全狀態(tài)采用ThreadLocal進(jìn)行處理,讓它們也成為線程安全的狀態(tài)宁炫,因?yàn)橛袪顟B(tài)的Bean就可以在多線程中共享了偿曙。

一般的Web應(yīng)用劃分為展現(xiàn)層、服務(wù)層和持久層三個(gè)層次羔巢,在不同的層中編寫(xiě)對(duì)應(yīng)的邏輯望忆,下層通過(guò)接口向上層開(kāi)放功能調(diào)用。在一般情況下竿秆,從接收請(qǐng)求到返回響應(yīng)所經(jīng)過(guò)的所有程序調(diào)用都同屬于一個(gè)線程启摄。

同一線程貫通三層這樣你就可以根據(jù)需要,將一些非線程安全的變量以ThreadLocal存放幽钢,在同一次請(qǐng)求響應(yīng)的調(diào)用線程中歉备,所有關(guān)聯(lián)的對(duì)象引用到的都是同一個(gè)變量。

下面的實(shí)例能夠體現(xiàn)Spring對(duì)有狀態(tài)Bean的改造思路:

TestDao:非線程安全

publicclassTestDao{privateConnectionconn;// ①一個(gè)非線程安全的變量publicvoidaddTopic()throwsSQLException{Statementstat=conn.createStatement();// ②引用非線程安全變量// …}

由于conn是成員變量匪燕,因?yàn)閍ddTopic()方法是非線程安全的蕾羊,必須在使用時(shí)創(chuàng)建一個(gè)新TopicDao實(shí)例(非singleton)喧笔。下面使用ThreadLocal對(duì)conn這個(gè)非線程安全的“狀態(tài)”進(jìn)行改造:

TestDao:線程安全

publicclassTestDaoNew{// ①使用ThreadLocal保存Connection變量?privatestaticThreadLocal<Connection>connThreadLocal=ThreadLocal.withInitial(Test::createConnection);?// 具體創(chuàng)建數(shù)據(jù)庫(kù)連接的方法?privatestaticConnectioncreateConnection(){??Connectionresult=null;??/**

? ? ?* create a real connection...

? ? ?* such as :

? ? ?* result = DriverManager.getConnection(dbUrl, dbUser, dbPwd);

? ? ?*/??returnresult;?}?// ③直接返回線程本地變量?publicstaticConnectiongetConnection(){??returnconnThreadLocal.get();?}// 具體操作?publicvoidaddTopic()throwsSQLException{??// ④從ThreadLocal中獲取線程對(duì)應(yīng)的Connection??Statementstat=getConnection().createStatement();??//....any other operation?}}

不同的線程在使用TopicDao時(shí),根據(jù)之前的深挖get具體操作龟再,判斷connThreadLocal.get()會(huì)去判斷是有map书闸,沒(méi)有則根據(jù)initivalValue創(chuàng)建一個(gè)Connection對(duì)象并添加到本地線程變量中,initivalValue對(duì)應(yīng)的值也就是上述的lamba表達(dá)式對(duì)應(yīng)的創(chuàng)建connection的方法返回的結(jié)果利凑,下次get則由于已經(jīng)有了浆劲,則會(huì)直接獲取已經(jīng)創(chuàng)建好的Connection,這樣截碴,就保證了不同的線程使用線程相關(guān)的Connection梳侨,而不會(huì)使用其它線程的Connection蛉威。因此日丹,這個(gè)TopicDao就可以做到singleton共享了。

當(dāng)然蚯嫌,這個(gè)例子本身很粗糙哲虾,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個(gè)方法共享Connection時(shí)不發(fā)生線程安全問(wèn)題,但無(wú)法和其它DAO共用同一個(gè)Connection择示,要做到同一事務(wù)多DAO共享同一Connection束凑,必須在一個(gè)共同的外部類(lèi)使用ThreadLocal保存Connection。

ConnectionManager.java

publicclassConnectionManager{privatestaticThreadLocal<Connection>connectionHolder=ThreadLocal.withInitial(()->{Connectionconn=null;try{conn=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","username","password");}catch(SQLExceptione){e.printStackTrace();}returnconn;});publicstaticConnectiongetConnection(){returnconnectionHolder.get();}}

線程隔離的秘密

秘密就就在于上述敘述的ThreadLocalMap這個(gè)類(lèi)栅盲。ThreadLocalMap是ThreadLocal類(lèi)的一個(gè)靜態(tài)內(nèi)部類(lèi)汪诉,它實(shí)現(xiàn)了鍵值對(duì)的設(shè)置和獲取(對(duì)比Map對(duì)象來(lái)理解)谈秫,每個(gè)線程中都有一個(gè)獨(dú)立的ThreadLocalMap副本扒寄,它所存儲(chǔ)的值,只能被當(dāng)前線程讀取和修改拟烫。ThreadLocal類(lèi)通過(guò)操作每一個(gè)線程特有的ThreadLocalMap副本该编,從而實(shí)現(xiàn)了變量訪問(wèn)在不同線程中的隔離。因?yàn)槊總€(gè)線程的變量都是自己特有的硕淑,完全不會(huì)有并發(fā)錯(cuò)誤课竣。還有一點(diǎn)就是,ThreadLocalMap存儲(chǔ)的鍵值對(duì)中的鍵是this對(duì)象指向的ThreadLocal對(duì)象置媳,而值就是你所設(shè)置的對(duì)象了于樟。

為了加深理解,我們接著看上面代碼中出現(xiàn)的getMap和createMap方法的實(shí)現(xiàn):

/**

? ? * Get the map associated with a ThreadLocal. Overridden in

? ? * InheritableThreadLocal.

? ? *

? ? * @param? t the current thread

? ? * @return the map

? ? */ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}/**

? ? * Create the map associated with a ThreadLocal. Overridden in

? ? * InheritableThreadLocal.

? ? *

? ? * @param t the current thread

? ? * @param firstValue value for the initial entry of the map

? ? * @param map the map to store.

? ? */voidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);}

小結(jié)

ThreadLocal是解決線程安全問(wèn)題一個(gè)很好的思路拇囊,它通過(guò)為每個(gè)線程提供一個(gè)獨(dú)立的變量副本解決了變量并發(fā)訪問(wèn)的沖突問(wèn)題迂曲。在很多情況下,ThreadLocal比直接使用synchronized同步機(jī)制解決線程安全問(wèn)題更簡(jiǎn)單寂拆,更方便奢米,且結(jié)果程序擁有更高的并發(fā)性抓韩。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鬓长,隨后出現(xiàn)的幾起案子谒拴,更是在濱河造成了極大的恐慌,老刑警劉巖涉波,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件英上,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡啤覆,警方通過(guò)查閱死者的電腦和手機(jī)苍日,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)窗声,“玉大人相恃,你說(shuō)我怎么就攤上這事”棵伲” “怎么了拦耐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)见剩。 經(jīng)常有香客問(wèn)我杀糯,道長(zhǎng),這世上最難降的妖魔是什么苍苞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任固翰,我火速辦了婚禮,結(jié)果婚禮上羹呵,老公的妹妹穿的比我還像新娘骂际。我一直安慰自己,他們只是感情好担巩,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布方援。 她就那樣靜靜地躺著,像睡著了一般涛癌。 火紅的嫁衣襯著肌膚如雪犯戏。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,760評(píng)論 1 289
  • 那天拳话,我揣著相機(jī)與錄音先匪,去河邊找鬼。 笑死弃衍,一個(gè)胖子當(dāng)著我的面吹牛呀非,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼岸裙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼猖败!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起降允,我...
    開(kāi)封第一講書(shū)人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤恩闻,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后剧董,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體幢尚,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年翅楼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尉剩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毅臊,死狀恐怖理茎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情褂微,我是刑警寧澤功蜓,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站宠蚂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏童社。R本人自食惡果不足惜求厕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扰楼。 院中可真熱鬧呀癣,春花似錦、人聲如沸弦赖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蹬竖。三九已至沼沈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間币厕,已是汗流浹背列另。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旦装,地道東北人页衙。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親店乐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子艰躺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348