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ā)性抓韩。