隨筆篇-ThreadLocal原理分析

1. 存取值分析

1.0 前言

ThreadLocal存取值都是借助ThreadLocalMap對(duì)象去進(jìn)行存取值,而ThreadLocalMap類是定義在ThreadLocal類中的一個(gè)靜態(tài)內(nèi)部類

image-20211110093901448

在再存取值時(shí)都是獲取當(dāng)前線程的ThreadLocalMap實(shí)例,即每個(gè)線程都會(huì)創(chuàng)建一個(gè)ThreadLocalMap類型對(duì)象,如下:

image-20211110094526201

該類型為實(shí)例變量,也就是說每個(gè)線程對(duì)象都會(huì)擁有一個(gè)threadLocalMap對(duì)象

ThreadLocalMap本質(zhì)就是一個(gè)就是Map類型,在其內(nèi)部也維護(hù)了一個(gè)Entry類型對(duì)象,如下:

image-20211110095259806

這樣就與HashMap類似,存儲(chǔ)的也是key-value類型數(shù)據(jù)

1.1 存值

從上文的表述中,存儲(chǔ)值都是通過ThreadLocalMap進(jìn)行操作,根據(jù)源碼可知,主要分為三個(gè)步驟:

  • 獲取當(dāng)前線程
  • 獲取ThreadLocalMap類型對(duì)象
  • 判斷是否為空,為空創(chuàng)建,否則存值

關(guān)于每個(gè)步驟詳細(xì)解釋如下:


獲取當(dāng)前線程

image-20211110100009387

根據(jù)線程實(shí)例獲取ThreadLocalMap類型對(duì)象

image-20211110100034078

而獲取ThreadLocalMap對(duì)象則需要傳入一個(gè)線程對(duì)象,查看getMap源碼可知,其實(shí)就是返回了線程對(duì)象的ThreadLocalMap實(shí)例對(duì)象

image-20211110100213242

而通過之前的1.0章節(jié)我們知道,線程對(duì)象里面的threadLocals對(duì)象并沒有實(shí)例化,只是給了null

因此當(dāng)?shù)谝淮潍@取threadLocals這個(gè)實(shí)例時(shí),最后的結(jié)果肯定為空


實(shí)例對(duì)象為空則創(chuàng)建ThreadLocalMap類型對(duì)象

從其源碼可知,當(dāng)實(shí)例對(duì)象為空的時(shí)候,就回去創(chuàng)建ThreadLocalMap,創(chuàng)建該類型對(duì)象需要傳入當(dāng)前線程對(duì)象,以及要存儲(chǔ)的值,如下:

image-20211110100534933

查看createMap源碼可知,就是new了一個(gè)ThreadLocalMap對(duì)象

  • key值為this也就是當(dāng)前的ThreadLocal對(duì)象
  • value 為具體要存入ThreadLocal的值

最后將創(chuàng)建好的對(duì)象賦值給 線程對(duì)象里面的ThreadLocalMap實(shí)例,源碼如下:

image-20211110101020896

這樣在一個(gè)線程中始終只有一個(gè)ThreadLocalMap類型對(duì)象,但是這個(gè)類型中可以存放多個(gè)ThreadLocal對(duì)象值


如果 map不為空,則將值存入ThreadLocalMap中,key為當(dāng)前的ThreadLocal對(duì)象

image-20211110101317189

上述步驟其流程圖如下:

image-20211110101710534

可能這里圖看的并不清晰,可以查看: 高清大圖

1.2 取值

ThreadLocal取值原理與存值原理相似,都是幾個(gè)固定步驟,如下:

  • 獲取當(dāng)前線程對(duì)象
  • 根據(jù)線程對(duì)象獲取ThreadLocalMap實(shí)例
  • 如果ThreadLocalMap實(shí)例不為空,則獲取值
  • 如果ThreadLocalMap實(shí)例為空,則調(diào)用setInitValue方法

關(guān)于這幾個(gè)步驟詳細(xì)解釋如下


獲取當(dāng)前線程對(duì)象

根據(jù)線程對(duì)象獲取ThreadLocalMap實(shí)例

image-20211110104401332

這兩個(gè)步驟在上文中進(jìn)行了詳細(xì)闡述,這里就不再過多贅述


如果ThreadLocalMap實(shí)例不為空,則獲取值

這里取值并不是直接從Map中獲取value,而是根據(jù)this(當(dāng)前ThreadLocal)對(duì)象取獲取Entry實(shí)體

如果Entry實(shí)體不為空,則獲取Entry的value,如下:

image-20211110105009595

如果ThreadLocalMap實(shí)例為空,則調(diào)用setInitValue方法

從源碼可知,當(dāng)ThreadLocalMap實(shí)例為空時(shí),則調(diào)用setInitValue方法,而該方法

則又是先調(diào)用了initialValue方法獲取value值,也就是存儲(chǔ)要初始化存儲(chǔ)的值

接下來就又是存值的幾個(gè)步驟,源碼如下:

image-20211110105344631

通過查看initiaValue方法可只,該方法為返回null

image-20211110105509348

但是如果子類重寫了該方法,返回并不是null,那么map中存儲(chǔ)的就不是null值


關(guān)于上述步驟流程圖如下:

image-20211110110125607

流程圖不太清晰,如果想要查看清晰,可以通過該網(wǎng)址查看:高清大圖

1.3 總結(jié)

從上文的表述中知道,往ThreadLocal中存值一共有兩種方式

  • 直接創(chuàng)建ThreadLocal對(duì)象,并且通過set方式進(jìn)行存值
  • 或者子類繼承ThreadLocal,然后重寫initialValue方法

當(dāng)然取值就只通過get方法了,一個(gè)線程從始至終就只有一個(gè)ThreadLocalMap對(duì)象

而一個(gè)ThreadLocalMap可以存儲(chǔ)多個(gè)ThreadLocal,即一個(gè)線程可以有多個(gè)ThreadLocal對(duì)象

當(dāng)然一個(gè)ThreadLocal對(duì)象只能存值一個(gè)實(shí)例對(duì)象,并不能存儲(chǔ)多個(gè)實(shí)例,原因在上文中也進(jìn)行了闡述

2. 內(nèi)存泄漏

2.0 前言

當(dāng)一個(gè)線程啟動(dòng),并且給創(chuàng)建了一個(gè)ThreadLocal對(duì)象,在堆棧中內(nèi)存簡圖如下:

image-20211110135040042

當(dāng)發(fā)生內(nèi)存泄漏時(shí),有可能時(shí)ThreadLocalMap里面的key發(fā)生泄漏,也有可能是Map發(fā)生value發(fā)生泄漏,因此接下來來詳細(xì)分析一下泄漏原因

2.1 分析

通過源碼可知,在ThreadLocalMap中,可以發(fā)現(xiàn)Entry繼承了WeakReference,且在其構(gòu)造方法中,將key通過調(diào)用父類方法進(jìn)行了弱引用處理,如下:

image-20211110135814132

弱引用是指這個(gè)如果對(duì)象的引用是一個(gè)弱引用,那么當(dāng)下一次發(fā)生GC的時(shí)候一定會(huì)被回收掉

從源碼知道,也就是ThreadLocalMap中的ThreadLocal對(duì)象一旦回收掉,也就是說key就為null了。

但是value確實(shí)一個(gè)強(qiáng)引用,即ThreadLocalMap中存在 key為null的value,如下:

image-20211110140440478

如果當(dāng)前線程對(duì)象,執(zhí)行一個(gè)耗時(shí)操作,長時(shí)間不被銷毀,也就意味著ThreadLocalMap中key為value的數(shù)據(jù),永遠(yuǎn)沒法被GC,因此可能發(fā)生內(nèi)存泄漏(OOM)

2.2 解決

JDK在設(shè)計(jì)時(shí)已經(jīng)考慮到這些問題,例如當(dāng)調(diào)用set(),remove(),rehash()方法會(huì)手動(dòng)清理key為null的數(shù)據(jù),例如查看set()方法調(diào)用鏈可知,最后調(diào)用了resize()方法,如下:

image-20211110142915770

resize方法中,會(huì)主動(dòng)的將key為null的數(shù)據(jù)也制空,以便防止內(nèi)存泄漏,如圖:

image.png

因此如果當(dāng)ThreadLocal發(fā)生內(nèi)存泄漏了,那么肯定是存在了key為null的數(shù)據(jù),所以在阿里開發(fā)規(guī)范中, 提到了如果ThreadLocal使用完成,那么應(yīng)該手動(dòng)調(diào)用一下remove

image-20211110144008399

思考:不是已經(jīng)做了優(yōu)化了么,怎么還會(huì)存在key為null的數(shù)據(jù)呢?

其實(shí)當(dāng)一個(gè)ThreadLocal不再被被使用,那么resize方法就不會(huì)調(diào)用,如果線程不停止,那么就會(huì)發(fā)生OOM有可能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容