ThreadLocal的作用以及應(yīng)用場(chǎng)景
ThreadLocal
算是一種并發(fā)容器吧,因?yàn)樗膬?nèi)部是有ThreadLocalMap
組成,ThreadLocal
是為了解決多線(xiàn)程情況下變量不能被共享的問(wèn)題,也就是多線(xiàn)程共享變量的問(wèn)題。
ThreadLocal
和Lock
以及Synchronized
的區(qū)別是:ThreadLocal
是給每個(gè)線(xiàn)程分配一個(gè)變量(對(duì)象),各個(gè)線(xiàn)程都存有變量的副本,這樣每個(gè)線(xiàn)程都是使用自己(變量)對(duì)象實(shí)例,使線(xiàn)程與線(xiàn)程之間進(jìn)行隔離;而Lock
和Synchronized
的方式是使線(xiàn)程有順序的執(zhí)行。
舉一個(gè)簡(jiǎn)單的例子:目前有100個(gè)學(xué)生等待簽字,但是老師只有一個(gè)筆,那老師只能按順序的分給每個(gè)學(xué)生,等待A學(xué)生簽字完成然后將筆交給B學(xué)生,這就類(lèi)似Lock
,Synchronized
的方式。而ThreadLocal
是,老師直接拿出一百個(gè)筆給每個(gè)學(xué)生;再效率提高的同事也要付出一個(gè)內(nèi)存消耗;也就是以空間換時(shí)間的概念
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶(hù)小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶(hù)、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
使用場(chǎng)景
Spring的事務(wù)隔離就是使用ThreadLocal
和AOP來(lái)解決的;主要是TransactionSynchronizationManager
這個(gè)類(lèi);
解決SimpleDateFormat
線(xiàn)程不安全問(wèn)題;
當(dāng)我們使用SimpleDateFormat
的parse()
方法的時(shí)候,parse()
方法會(huì)先調(diào)用Calendar.clear()
方法,然后調(diào)用Calendar.add()
方法,如果一個(gè)線(xiàn)程先調(diào)用了add()
方法,然后另一個(gè)線(xiàn)程調(diào)用了clear()
方法;這時(shí)候parse()
方法就會(huì)出現(xiàn)解析錯(cuò)誤;如果不信我們可以來(lái)個(gè)例子:
publicclassSimpleDateFormatTest{
privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyy-MM-dd");
publicstaticvoidmain(String[]args){
for(inti=0;i50;i++){
Threadthread=newThread(newRunnable(){
@Override
publicvoidrun(){
dateFormat();
}
});
thread.start();
}
}
/**
*字符串轉(zhuǎn)成日期類(lèi)型
*/
publicstaticvoiddateFormat(){
try{
simpleDateFormat.parse("2021-5-27");
}catch(ParseExceptione){
e.printStackTrace();
}
}
}
這里我們只啟動(dòng)了50個(gè)線(xiàn)程問(wèn)題就會(huì)出現(xiàn),其實(shí)看巧不巧,有時(shí)候只有10個(gè)線(xiàn)程的情況就會(huì)出錯(cuò):
Exceptioninthread"Thread-40"java.lang.NumberFormatException:Forinputstring:""
atjava.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
atjava.lang.Long.parseLong(Long.java:601)
atjava.lang.Long.parseLong(Long.java:631)
atjava.text.DigitList.getLong(DigitList.java:195)
atjava.text.DecimalFormat.parse(DecimalFormat.java:2084)
atjava.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
atjava.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
atjava.text.DateFormat.parse(DateFormat.java:364)
atcn.haoxy.use.lock.sdf.SimpleDateFormatTest.dateFormat(SimpleDateFormatTest.java:36)
atcn.haoxy.use.lock.sdf.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:23)
atjava.lang.Thread.run(Thread.java:748)
Exceptioninthread"Thread-43"java.lang.NumberFormatException:multiplepoints
atsun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
atsun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
atjava.lang.Double.parseDouble(Double.java:538)
atjava.text.DigitList.getDouble(DigitList.java:169)
atjava.text.DecimalFormat.parse(DecimalFormat.java:2089)
atjava.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
atjava.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
atjava.text.DateFormat.parse(DateFormat.java:364)
at.............
其實(shí)解決這個(gè)問(wèn)題很簡(jiǎn)單,讓每個(gè)線(xiàn)程new一個(gè)自己的SimpleDateFormat
,但是如果100個(gè)線(xiàn)程都要new100個(gè)SimpleDateFormat
嗎?
當(dāng)然我們不能這么做,我們可以借助線(xiàn)程池加上ThreadLocal
來(lái)解決這個(gè)問(wèn)題:
publicclassSimpleDateFormatTest{
privatestaticThreadLocallocal=newThreadLocal(){
@Override
//初始化線(xiàn)程本地變量
protectedSimpleDateFormatinitialValue(){
returnnewSimpleDateFormat("yyyy-MM-dd");
}
};
publicstaticvoidmain(String[]args){
ExecutorServicees=Executors.newCachedThreadPool();
for(inti=0;i500;i++){
es.execute(()->{
//調(diào)用字符串轉(zhuǎn)成日期方法
dateFormat();
});
}
es.shutdown();
}
/**
*字符串轉(zhuǎn)成日期類(lèi)型
*/
publicstaticvoiddateFormat(){
try{
//ThreadLocal中的get()方法
local.get().parse("2021-5-27");
}catch(ParseExceptione){
e.printStackTrace();
}
}
}
這樣就優(yōu)雅的解決了線(xiàn)程安全問(wèn)題;
解決過(guò)度傳參問(wèn)題;例如一個(gè)方法中要調(diào)用好多個(gè)方法,每個(gè)方法都需要傳遞參數(shù);例如下面示例:
voidwork(Useruser){
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
用了ThreadLocal
之后:
publicclassThreadLocalStu{
privatestaticThreadLocaluserThreadLocal=newThreadLocal<>();
voidwork(Useruser){
try{
userThreadLocal.set(user);
getInfo();
checkInfo();
someThing();
}finally{
userThreadLocal.remove();
}
}
voidsetInfo(){
Useru=userThreadLocal.get();
//.....
}
voidcheckInfo(){
Useru=userThreadLocal.get();
//....
}
voidsomeThing(){
Useru=userThreadLocal.get();
//....
}
}
每個(gè)線(xiàn)程內(nèi)需要保存全局變量(比如在登錄成功后將用戶(hù)信息存到ThreadLocal
里,然后當(dāng)前線(xiàn)程操作的業(yè)務(wù)邏輯直接get取就完事了,有效的避免的參數(shù)來(lái)回傳遞的麻煩之處),一定層級(jí)上減少代碼耦合度。
- 比如存儲(chǔ) 交易id等信息。每個(gè)線(xiàn)程私有。
- 比如aop里記錄日志需要before記錄請(qǐng)求id,end拿出請(qǐng)求id,這也可以。
-
比如jdbc連接池(很典型的一個(gè)
ThreadLocal
用法) - ....等等....
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶(hù)小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶(hù)、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
原理分析
上面我們基本上知道了ThreadLocal
的使用方式以及應(yīng)用場(chǎng)景,當(dāng)然應(yīng)用場(chǎng)景不止這些這只是工作中常用到的場(chǎng)景;下面我們對(duì)它的原理進(jìn)行分析;
我們先看一下它的set()
方法;
publicvoidset(Tvalue){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}
是不是特別簡(jiǎn)單,首先獲取當(dāng)前線(xiàn)程,用當(dāng)前線(xiàn)程作為key,去獲取ThreadLocalMap
,然后判斷map是否為空,不為空就將當(dāng)前線(xiàn)程作為key,傳入的value作為map的value值;如果為空就創(chuàng)建一個(gè)ThreadLocalMap
,然后將key和value方進(jìn)去;從這里可以看出value值是存放到ThreadLocalMap
中;
然后我們看看ThreadLocalMap
是怎么來(lái)的?先看下getMap()
方法:
//在Thread類(lèi)中維護(hù)了threadLocals變量,注意是Thread類(lèi)
ThreadLocal.ThreadLocalMapthreadLocals=null;
//在ThreadLocal類(lèi)中的getMap()方法
ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
}
這就能解釋每個(gè)線(xiàn)程中都有一個(gè)ThreadLocalMap
,因?yàn)?code style="font-size:14px;padding:2px 4px;margin-right:2px;margin-left:2px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">ThreadLocalMap的引用在Thread中維護(hù);這就確保了線(xiàn)程間的隔離;
我們繼續(xù)回到set()
方法,看到當(dāng)map等于空的時(shí)候createMap(t, value);
voidcreateMap(Threadt,TfirstValue){
t.threadLocals=newThreadLocalMap(this,firstValue);
}
這里就是new了一個(gè)ThreadLocalMap
然后賦值給threadLocals
成員變量;ThreadLocalMap
構(gòu)造方法:
ThreadLocalMap(ThreadLocal>firstKey,ObjectfirstValue){
//初始化一個(gè)Entry
table=newEntry[INITIAL_CAPACITY];
//計(jì)算key應(yīng)該存放的位置
inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);
//將Entry放到指定位置
table[i]=newEntry(firstKey,firstValue);
size=1;
//設(shè)置數(shù)組的大小16*2/3=10,類(lèi)似HashMap中的0.75*16=12
setThreshold(INITIAL_CAPACITY);
}
這里寫(xiě)有個(gè)大概的印象,后面對(duì)ThreadLocalMap
內(nèi)部結(jié)構(gòu)還會(huì)進(jìn)行詳細(xì)的講解;
下面我們?cè)偃タ匆幌?code style="font-size:14px;padding:2px 4px;margin-right:2px;margin-left:2px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">get()方法:
publicTget(){
Threadt=Thread.currentThread();
//用當(dāng)前線(xiàn)程作為key去獲取ThreadLocalMap
ThreadLocalMapmap=getMap(t);
if(map!=null){
//map不為空,然后獲取map中的Entry
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
@SuppressWarnings("unchecked")
//如果Entry不為空就獲取對(duì)應(yīng)的value值
Tresult=(T)e.value;
returnresult;
}
}
//如果map為空或者entry為空的話(huà)通過(guò)該方法初始化,并返回該方法的value
returnsetInitialValue();
}
get()
方法和set()
都比較容易理解,如果map等于空的時(shí)候或者entry等于空的時(shí)候我們看看setInitialValue()
方法做了什么事:
privateTsetInitialValue(){
//初始化變量值由子類(lèi)去實(shí)現(xiàn)并初始化變量
Tvalue=initialValue();
Threadt=Thread.currentThread();
//這里再次getMap();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
//和set()方法中的
createMap(t,value);
returnvalue;
}
下面我們?cè)偃タ匆幌?code style="font-size:14px;padding:2px 4px;margin-right:2px;margin-left:2px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">ThreadLocal中的initialValue()
方法:
protectedTinitialValue(){
returnnull;
}
設(shè)置初始值,由子類(lèi)去實(shí)現(xiàn);就例如我們上面的例子,重寫(xiě)ThreadLocal
類(lèi)中的initialValue()
方法:
privatestaticThreadLocallocal=newThreadLocal(){
@Override
//初始化線(xiàn)程本地變量
protectedSimpleDateFormatinitialValue(){
returnnewSimpleDateFormat("yyyy-MM-dd");
}
};
createMap()
方法和上面set()
方法中createMap()
方法同一個(gè),就不過(guò)多的敘述了;剩下還有一個(gè)removve()
方法
publicvoidremove(){
ThreadLocalMapm=getMap(Thread.currentThread());
if(m!=null)
//2.從map中刪除以當(dāng)前threadLocal實(shí)例為key的鍵值對(duì)
m.remove(this);
}
源碼的講解就到這里,也都比較好理解,下面我們看看ThreadLocalMap
的底層結(jié)構(gòu)
ThreadLocalMap的底層結(jié)構(gòu)
上面我們已經(jīng)了解了ThreadLocal
的使用場(chǎng)景以及它比較重要的幾個(gè)方法;下面我們?cè)偃ニ膬?nèi)部結(jié)構(gòu);經(jīng)過(guò)上的源碼分析我們可以看到數(shù)據(jù)其實(shí)都是存放到了ThreadLocal
中的內(nèi)部類(lèi)ThreadLocalMap
中;而ThreadLocalMap
中又維護(hù)了一個(gè)Entry對(duì)象,也就說(shuō)數(shù)據(jù)最終是存放到Entry對(duì)象中的;
staticclassThreadLocalMap{
staticclassEntryextendsWeakReference<ThreadLocal>>{
/**ThevalueassociatedwiththisThreadLocal.*/
Objectvalue;
Entry(ThreadLocal>k,Objectv){
super(k);
value=v;
}
}
ThreadLocalMap(ThreadLocal>firstKey,ObjectfirstValue){
table=newEntry[INITIAL_CAPACITY];
inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);
table[i]=newEntry(firstKey,firstValue);
size=1;
setThreshold(INITIAL_CAPACITY);
}
//....................
}
Entry的構(gòu)造方法是以當(dāng)前線(xiàn)程為key,變量值Object為value進(jìn)行存儲(chǔ)的;在上面的源碼中ThreadLocalMap
的構(gòu)造方法中也涉及到了Entry;看到Entry是一個(gè)數(shù)組;初始化長(zhǎng)度為INITIAL_CAPACITY = 16;
因?yàn)?Entry 繼承了 WeakReference
,在 Entry 的構(gòu)造方法中,調(diào)用了 super(k)
方法就會(huì)將 threadLocal
實(shí)例包裝成一個(gè) WeakReferenece
。這也是ThreadLocal
會(huì)產(chǎn)生內(nèi)存泄露的原因;
內(nèi)存泄露產(chǎn)生的原因

如圖所示存在一條引用鏈: Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value
,經(jīng)過(guò)上面的講解我們知道ThreadLocal
作為Key,但是被設(shè)置成了弱引用,弱引用在JVM垃圾回收時(shí)是優(yōu)先回收的,就是說(shuō)無(wú)論內(nèi)存是否足夠弱引用對(duì)象都會(huì)被回收;弱引用的生命周期比較短;當(dāng)發(fā)生一次GC的時(shí)候就會(huì)變成如下:

TreadLocalMap
中出現(xiàn)了Key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果線(xiàn)程遲遲不結(jié)束(也就是說(shuō)這條引用鏈無(wú)意義的一直存在)就會(huì)造成value永遠(yuǎn)無(wú)法回收造成內(nèi)存泄露;如果當(dāng)前線(xiàn)程運(yùn)行結(jié)束Thread,ThreadLocalMap
,Entry之間沒(méi)有了引用鏈,在垃圾回收的時(shí)候就會(huì)被回收;但是在開(kāi)發(fā)中我們都是使用線(xiàn)程池的方式,線(xiàn)程池的復(fù)用不會(huì)主動(dòng)結(jié)束;所以還是會(huì)存在內(nèi)存泄露問(wèn)題;
解決方法也很簡(jiǎn)單,就是在使用完之后主動(dòng)調(diào)用remove()
方法釋放掉;
解決Hash沖突
記得在大學(xué)學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的時(shí)候?qū)W習(xí)了很多種解決hash沖突的方法;例如:
線(xiàn)性探測(cè)法(開(kāi)放地址法的一種): 計(jì)算出的散列地址如果已被占用,則按順序找下一個(gè)空位。如果找到末尾還沒(méi)有找到空位置就從頭重新開(kāi)始找;

二次探測(cè)法(開(kāi)放地址法的一種)

鏈地址法:鏈地址是對(duì)每一個(gè)同義詞都建一個(gè)單鏈表來(lái)解決沖突,HashMap采用的是這種方法;

多重Hash法: 在key沖突的情況下多重hash,直到不沖突為止,這種方式不易產(chǎn)生堆積但是計(jì)算量太大;
公共溢出區(qū)法: 這種方式需要兩個(gè)表,一個(gè)存基礎(chǔ)數(shù)據(jù),另一個(gè)存放沖突數(shù)據(jù)稱(chēng)為溢出表;
上面的圖片都是在網(wǎng)上找到的一些資料,和大學(xué)時(shí)學(xué)習(xí)時(shí)的差不多我就直接拿來(lái)用了;也當(dāng)自己復(fù)習(xí)了一遍;
介紹了那么多解決Hash沖突的方法,那ThreadLocalMap
使用的哪一種方法呢?我們可以看一下源碼:
privatevoidset(ThreadLocal>key,Objectvalue){
Entry[]tab=table;
intlen=tab.length;
//根據(jù)HashCode&數(shù)組長(zhǎng)度計(jì)算出數(shù)組該存放的位置
inti=key.threadLocalHashCode&(len-1);
//遍歷Entry數(shù)組中的元素
for(Entrye=tab[i];
e!=null;
e=tab[i=nextIndex(i,len)]){
ThreadLocal>k=e.get();
//如果這個(gè)Entry對(duì)象的key正好是即將設(shè)置的key,那么就刷新Entry中的value;
if(k==key){
e.value=value;
return;
}
//entry!=null,key==null時(shí),說(shuō)明threadLcoal這key已經(jīng)被GC了,這里就是上面說(shuō)到
//會(huì)有內(nèi)存泄露的地方,當(dāng)然作者也知道這種情況的存在,所以這里做了一個(gè)判斷進(jìn)行解決臟的
//entry(數(shù)組中不想存有過(guò)時(shí)的entry),但是也不能解決泄露問(wèn)題,因?yàn)榕fvalue還存在沒(méi)有消失
if(k==null){
//用當(dāng)前插入的值代替掉這個(gè)key為null的“臟”entry
replaceStaleEntry(key,value,i);
return;
}
}
//新建entry并插入table中i處
tab[i]=newEntry(key,value);
intsz=++size;
if(!cleanSomeSlots(i,sz)&&sz>=threshold)
rehash();
}
從這里我們可以看出使用的是線(xiàn)性探測(cè)的方式來(lái)解決hash沖突!
源碼中通過(guò)nextIndex(i, len)
方法解決 hash 沖突的問(wèn)題,該方法為((i + 1 < len) ? i + 1 : 0);
,也就是不斷往后線(xiàn)性探測(cè),直到找到一個(gè)空的位置,當(dāng)?shù)焦1砟┪驳臅r(shí)候還沒(méi)有找到空位置再?gòu)?0 開(kāi)始找,成環(huán)形!
使用ThreadLocal時(shí)對(duì)象存在哪里?
在java中,棧內(nèi)存歸屬于單個(gè)線(xiàn)程,每個(gè)線(xiàn)程都會(huì)有一個(gè)棧內(nèi)存,其存儲(chǔ)的變量只能在其所屬線(xiàn)程中可見(jiàn),即棧內(nèi)存可以理解成線(xiàn)程的私有變量,而堆內(nèi)存中的變量對(duì)所有線(xiàn)程可見(jiàn),可以被所有線(xiàn)程訪問(wèn)!
那么ThreadLocal
的實(shí)例以及它的值是不是存放在棧上呢?其實(shí)不是的,因?yàn)?code style="font-size:14px;padding:2px 4px;margin-right:2px;margin-left:2px;color:rgb(30,107,184);background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">ThreadLocal的實(shí)例實(shí)際上也是被其創(chuàng)建的類(lèi)持有,(更頂端應(yīng)該是被線(xiàn)程持有),而ThreadLocal
的值其實(shí)也是被線(xiàn)程實(shí)例持有,它們都是位于堆上,只是通過(guò)一些技巧將可見(jiàn)性修改成了線(xiàn)程可見(jiàn)。
審核編輯 :李倩
-
代碼
+關(guān)注
關(guān)注
30文章
4900瀏覽量
70795 -
spring
+關(guān)注
關(guān)注
0文章
340瀏覽量
15096 -
線(xiàn)程
+關(guān)注
關(guān)注
0文章
508瀏覽量
20243
原文標(biāo)題:ThreadLocal 你真的用不上嗎?
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
評(píng)論