來(lái)探討一下最近面試問(wèn)的ThreadLocal問(wèn)題
中高級(jí)階段開(kāi)發(fā)者出去面試,應(yīng)該躲不開(kāi)ThreadLocal相關(guān)問(wèn)題,本文就常見(jiàn)問(wèn)題做出一些解答,歡迎留言探討。
ThreadLocal為java并發(fā)提供了一個(gè)新的思路, 它用來(lái)存儲(chǔ)Thread的局部變量, 從而達(dá)到各個(gè)Thread之間的隔離運(yùn)行。它被廣泛應(yīng)用于框架之間的用戶資源隔離、事務(wù)隔離等。
但是用不好會(huì)導(dǎo)致內(nèi)存泄漏, 本文重點(diǎn)用于對(duì)它的使用過(guò)程的疑難解答, 相信仔細(xì)閱讀完后的朋友可以隨心所欲的安全使用它。
內(nèi)存泄漏原因探索
ThreadLocal操作不當(dāng)會(huì)引發(fā)內(nèi)存泄露,最主要的原因在于它的內(nèi)部類ThreadLocalMap中的Entry的設(shè)計(jì)。
Entry繼承了WeakReference,即Entry的key是弱引用,所以key'會(huì)在垃圾回收的時(shí)候被回收掉, 而key對(duì)應(yīng)的value則不會(huì)被回收, 這樣會(huì)導(dǎo)致一種現(xiàn)象:key為null,value有值。
key為空的話value是無(wú)效數(shù)據(jù),久而久之,value累加就會(huì)導(dǎo)致內(nèi)存泄漏。
static?class?ThreadLocalMap?{
???????static?class?Entry?extends?WeakReference<ThreadLocal>>?{
????????????Object?value;
????????????Entry(ThreadLocal>?k,?Object?v)?{
????????????????super(k);
????????????????value?=?v;
????????????}
????????}
????...
}
怎么解決這個(gè)內(nèi)存泄漏問(wèn)題
每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)。因?yàn)樗膔emove方法會(huì)主動(dòng)將當(dāng)前的key和value(Entry)進(jìn)行清除。
private?void?remove(ThreadLocal>?key)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????int?i?=?key.threadLocalHashCode?&?(len-1);
????for?(Entry?e?=?tab[i];
?????????e?!=?null;
?????????e?=?tab[i?=?nextIndex(i,?len)])?{
????????if?(e.get()?==?key)?{
????????????e.clear();?//?清除key
????????????expungeStaleEntry(i);??//?清除value
????????????return;
????????}
????}
}
e.clear()用于清除Entry的key,它調(diào)用的是WeakReference中的方法:this.referent = null
expungeStaleEntry(i)用于清除Entry對(duì)應(yīng)的value, 這個(gè)后面會(huì)詳細(xì)講。
JDK開(kāi)發(fā)者是如何避免內(nèi)存泄漏的
ThreadLocal的設(shè)計(jì)者也意識(shí)到了這一點(diǎn)(內(nèi)存泄漏), 他們?cè)谝恍┓椒ㄖ新窳藢?duì)key=null的value擦除操作。
這里拿ThreadLocal提供的get()方法舉例,它調(diào)用了ThreadLocalMap#getEntry()方法,對(duì)key進(jìn)行了校驗(yàn)和對(duì)null key進(jìn)行擦除。
private?Entry?getEntry(ThreadLocal>?key)?{
????//?拿到索引位置
????int?i?=?key.threadLocalHashCode?&?(table.length?-?1);
????Entry?e?=?table[i];
????if?(e?!=?null?&&?e.get()?==?key)
????????return?e;
????else
????????return?getEntryAfterMiss(key,?i,?e);
}
如果key為null, 則會(huì)調(diào)用getEntryAfterMiss()方法,在這個(gè)方法中,如果k == null , 則調(diào)用expungeStaleEntry(i);方法。
private?Entry?getEntryAfterMiss(ThreadLocal>?key,?int?i,?Entry?e)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????while?(e?!=?null)?{
????????ThreadLocal>?k?=?e.get();
????????if?(k?==?key)
????????????return?e;
????????if?(k?==?null)
????????????expungeStaleEntry(i);
????????else
????????????i?=?nextIndex(i,?len);
????????e?=?tab[i];
????}
????return?null;
????}
expungeStaleEntry(i)方法完成了對(duì)key=null 的key所對(duì)應(yīng)的value進(jìn)行賦空, 釋放了空間避免內(nèi)存泄漏。
同時(shí)它遍歷下一個(gè)key為空的entry, 并將value賦值為null, 等待下次GC釋放掉其空間。
private?int?expungeStaleEntry(int?staleSlot)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????//?expunge?entry?at?staleSlot
????tab[staleSlot].value?=?null;
????tab[staleSlot]?=?null;
????size--;
????//?Rehash?until?we?encounter?null
????Entry?e;
????int?i;
????//?遍歷下一個(gè)key為空的entry,?并將value指向null
????for?(i?=?nextIndex(staleSlot,?len);
?????????(e?=?tab[i])?!=?null;
?????????i?=?nextIndex(i,?len))?{
????????ThreadLocal>?k?=?e.get();
????????if?(k?==?null)?{
????????????e.value?=?null;
????????????tab[i]?=?null;
????????????size--;
????????}?else?{
????????????int?h?=?k.threadLocalHashCode?&?(len?-?1);
????????????if?(h?!=?i)?{
????????????????tab[i]?=?null;
????????????????//?Unlike?Knuth?6.4?Algorithm?R,?we?must?scan?until
????????????????//?null?because?multiple?entries?could?have?been?stale.
????????????????while?(tab[h]?!=?null)
????????????????????h?=?nextIndex(h,?len);
????????????????tab[h]?=?e;
????????????}
????????}
????}
????return?i;
}
同理, set()方法最終也是調(diào)用該方法(expungeStaleEntry), 調(diào)用路徑: set(T value)->map.set(this, value)->rehash()->expungeStaleEntries()
remove方法remove()->ThreadLocalMap.remove(this)->expungeStaleEntry(i)
這樣做, 也只能說(shuō)盡可能避免內(nèi)存泄漏, 但并不會(huì)完全解決內(nèi)存泄漏這個(gè)問(wèn)題。比如極端情況下我們只創(chuàng)建ThreadLocal但不調(diào)用set、get、remove方法等。所以最能解決問(wèn)題的辦法就是用完ThreadLocal后手動(dòng)調(diào)用remove().
手動(dòng)釋放ThreadLocal遺留存儲(chǔ)?你怎么去設(shè)計(jì)/實(shí)現(xiàn)?
這里主要是強(qiáng)化一下手動(dòng)remove的思想和必要性,設(shè)計(jì)思想與連接池類似。
包裝其父類remove方法為靜態(tài)方法,如果是spring項(xiàng)目, 可以借助于bean的聲明周期, 在攔截器的afterCompletion階段進(jìn)行調(diào)用。
弱引用導(dǎo)致內(nèi)存泄漏,那為什么key不設(shè)置為強(qiáng)引用
這個(gè)問(wèn)題就比較有深度了,是你談薪的小小資本。
如果key設(shè)置為強(qiáng)引用, 當(dāng)threadLocal實(shí)例釋放后, threadLocal=null, 但是threadLocal會(huì)有強(qiáng)引用指向threadLocalMap,threadLocalMap.Entry又強(qiáng)引用threadLocal, 這樣會(huì)導(dǎo)致threadLocal不能正常被GC回收。
弱引用雖然會(huì)引起內(nèi)存泄漏, 但是也有set、get、remove方法操作對(duì)null key進(jìn)行擦除的補(bǔ)救措施, 方案上略勝一籌。
線程執(zhí)行結(jié)束后會(huì)不會(huì)自動(dòng)清空Entry的value
一并考察了你的gc基礎(chǔ)。
事實(shí)上,當(dāng)currentThread執(zhí)行結(jié)束后, threadLocalMap變得不可達(dá)從而被回收,Entry等也就都被回收了,但這個(gè)環(huán)境就要求不對(duì)Thread進(jìn)行復(fù)用,但是我們項(xiàng)目中經(jīng)常會(huì)復(fù)用線程來(lái)提高性能, 所以currentThread一般不會(huì)處于終止?fàn)顟B(tài)。
Thread和ThreadLocal有什么聯(lián)系呢
ThreadLocal的概念。
Thread和ThreadLocal是綁定的, ThreadLocal依賴于Thread去執(zhí)行, Thread將需要隔離的數(shù)據(jù)存放到ThreadLocal(準(zhǔn)確的講是ThreadLocalMap)中, 來(lái)實(shí)現(xiàn)多線程處理。
相關(guān)問(wèn)題擴(kuò)展
加分項(xiàng)來(lái)了。
spring如何處理bean多線程下的并發(fā)問(wèn)題
ThreadLocal天生為解決相同變量的訪問(wèn)沖突問(wèn)題, 所以這個(gè)對(duì)于spring的默認(rèn)單例bean的多線程訪問(wèn)是一個(gè)完美的解決方案。spring也確實(shí)是用了ThreadLocal來(lái)處理多線程下相同變量并發(fā)的線程安全問(wèn)題。
spring 如何保證數(shù)據(jù)庫(kù)事務(wù)在同一個(gè)連接下執(zhí)行的
要想實(shí)現(xiàn)jdbc事務(wù), 就必須是在同一個(gè)連接對(duì)象中操作, 多個(gè)連接下事務(wù)就會(huì)不可控, 需要借助分布式事務(wù)完成。那spring 如何保證數(shù)據(jù)庫(kù)事務(wù)在同一個(gè)連接下執(zhí)行的呢?
DataSourceTransactionManager 是spring的數(shù)據(jù)源事務(wù)管理器, 它會(huì)在你調(diào)用getConnection()的時(shí)候從數(shù)據(jù)庫(kù)連接池中獲取一個(gè)connection, 然后將其與ThreadLocal綁定, 事務(wù)完成后解除綁定。這樣就保證了事務(wù)在同一連接下完成。
概要源碼:
1.事務(wù)開(kāi)始階段:org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource

2.事務(wù)結(jié)束階段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource

推薦閱讀:
喜歡我可以給我設(shè)為星標(biāo)哦

好文章,我?在看?

