?Thread Local深度解析,你學(xué)會(huì)了嗎?
今天,有個(gè)朋友問(wèn)我說(shuō)他想在并發(fā)條件下統(tǒng)計(jì)接口的耗時(shí)以及日期,并做一個(gè)記錄在最后統(tǒng)一保存,這里我就直接想到了ThreadLocal,其實(shí)我用ThreadLocal的場(chǎng)景還挺多的,畢竟項(xiàng)目需要,其實(shí)一直都想對(duì)ThreadLocal做一個(gè)總結(jié),擇日不如撞日就現(xiàn)在動(dòng)手吧。
ThreadLocal概念
ThreadLocal也叫做本地線程變量,ThreadLocal中填充的是當(dāng)前線程的變量,該變量對(duì)其他線程是隔離的,ThreadLocal在每個(gè)線程中都創(chuàng)建了一個(gè)變量副本,所以每個(gè)線程中的ThreadLocal都是一個(gè)獨(dú)立的副本,自己可以訪問(wèn)自己線程內(nèi)部的副本變量互不干擾。
ThreadLocal使用場(chǎng)景
ThreadLocal的使用也要看情況來(lái)定,按個(gè)人理解ThreadLocal大致會(huì)使用到以下場(chǎng)景:
- 需要全局獲取變量(保證這個(gè)變量在全局中的一致性)
- 需要解決線程安全的場(chǎng)景(例如:記錄每個(gè)請(qǐng)求的一些信息,保存到日志表中)
- 父子線程需要共享數(shù)據(jù)(例如:需要子線程的結(jié)果回調(diào)給父線程,如何保存它的唯一性)
說(shuō)白了ThreadLocal就是做數(shù)據(jù)隔離,每條線程的ThreadLocal都是隔離的互不干擾,其實(shí)就是為了防止多線程環(huán)境下變量被其他線程篡改,只要記住這點(diǎn)在工作中什么場(chǎng)景下會(huì)使用到就一目了然了。
實(shí)際上Spring就是采用了Threadlocal來(lái)實(shí)現(xiàn)單個(gè)線程中的數(shù)據(jù)庫(kù)操作使用的是同一個(gè)數(shù)據(jù)庫(kù)連接,采用Threadlocal可以使業(yè)務(wù)層使用事務(wù)的時(shí)候不需要去管理connection對(duì)象,通過(guò)傳播級(jí)別就能管理多個(gè)事務(wù)配置之間的切換,掛起和恢復(fù)。
Spring框架里面就是用的ThreadLocal來(lái)實(shí)現(xiàn)這種隔離,主要是在TransactionSynchronizationManager這個(gè)類(lèi)里面,代碼如下所示:
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
注意:在Spring5.2以后的版本Spring事務(wù)隔離從ThreadLocal換成了Mono響應(yīng)式編程來(lái)實(shí)現(xiàn)隔離。
圖片
ThreadLocal源碼分析
圖片
從源碼上看其實(shí)ThreadLocal的set方法并不復(fù)雜
- 獲取當(dāng)前線程對(duì)象Thread.currentThread();
- 獲取線程變量ThreadLocalMap map = getMap(t);
- 如果不為空則賦值map.set(this,value);
- 如果為空,初始化該線程對(duì)象的map變量,其中key為當(dāng)前的threadlocal變量createMap(t,value);
再看看ThreadLocal的get方法
圖片
圖片
- 返回當(dāng)前線程變量的副本中的值,如果該變量沒(méi)有當(dāng)前線程的值,則先調(diào)用initialValue方法的返回值
- initialValue方法中繼續(xù)獲取當(dāng)前線程變量(Key為當(dāng)前線程)而Value設(shè)置為null
- 如果當(dāng)前線程副本變量為空那么重新創(chuàng)建當(dāng)前線程的Map(Key為當(dāng)前線程,Value為null)
ThreadLocal如何做到線程隔離?
上面分析了ThreadLocal的set()和get()源碼,在通過(guò)get()方法獲取當(dāng)前線程中副本變量為null那么直接創(chuàng)建一個(gè)ThreadLocalMap:
圖片
從這里入手,看一下t.threadLocals。
圖片
注釋說(shuō)得很清楚:ThreadLocal屬于當(dāng)前這個(gè)線程的。
注意:這個(gè)ThreadLocalMap是一個(gè)靜態(tài)內(nèi)部類(lèi)。
圖片
ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. No operations are exported outside of the ThreadLocal class. The class is package private to allow declaration of fields in class Thread. To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.
到此為止其實(shí)ThreadLocal的數(shù)據(jù)隔離的真相就出來(lái)了,說(shuō)白了每個(gè)線程Thread都維護(hù)了自己的一個(gè)threadLocals變量,當(dāng)線程創(chuàng)建ThreadLocal的時(shí)候,實(shí)際上數(shù)據(jù)是存在自己的線程Thread的threadLocals變量里面,可以看出來(lái)這個(gè)ThreadLocalMap這個(gè)類(lèi)只有一份,在線程中,所以實(shí)現(xiàn)了線程之間的隔離。
ThreadLocalMap底層原理
圖片
雖然看著ThreadLocalMap很像是HashMap,實(shí)際上并沒(méi)有實(shí)現(xiàn)Map接口,而是它的內(nèi)部類(lèi)Entry繼承了WeakReference這個(gè)弱引用,也就是說(shuō)不存在鏈表的關(guān)系了。
接下來(lái)我們來(lái)看一下ThreadLocalMap的set()方法(這里圖片沒(méi)有截全):
圖片
ThreadLocalMap在存儲(chǔ)的時(shí)候每次都會(huì)給每一個(gè)ThreadLocal對(duì)象一個(gè)threadLocalHashCode,在插入過(guò)程中,根據(jù)ThreadLocal對(duì)象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len - 1);
接下來(lái)判斷如果當(dāng)前位置為null,就初始化一個(gè)Entry對(duì)象放在位置上。
圖片
如果當(dāng)前位置i不為空,又剛好這個(gè)Entry對(duì)象的key正好是即將設(shè)置的key,那么就覆蓋Entry中的value。
圖片
如果位置i不為null并且key不等于 entry,那么就找下一個(gè)空位置,直到位置為空為止然后存放。
在get的時(shí)候就會(huì)根據(jù)ThreadLocal對(duì)象的Hash值,定位到相應(yīng)位置,然后判斷該位置Entry對(duì)象中的key是否和get的key一致,如果不一致,就判斷下個(gè)位置。
如何共享ThreadLocal中的數(shù)據(jù)?
使用 InheritableThreadLocal可以實(shí)現(xiàn)多個(gè)線程訪問(wèn)ThreadLocal的值。
問(wèn)題是它們之間是如何實(shí)現(xiàn)傳遞的?
其實(shí)邏輯很簡(jiǎn)單,繼續(xù)看Thread的源碼,看下初始化的時(shí)候Thread.init做了什么操作:
圖片
如果線程的inheritThreadLocals變量不為空的話,并且父線程的inheritThreadLocals不為空的話,就把線程的inheritThreadLocals給當(dāng)前線程的inheritThreadLocals。
圖片
關(guān)于ThreadLocal內(nèi)存泄露
ThreadLocal使用不當(dāng)也會(huì)出現(xiàn)問(wèn)題:那就是內(nèi)存泄露。
繼續(xù)查看最開(kāi)始存儲(chǔ)數(shù)據(jù)的Entry類(lèi)的源碼:
圖片
其實(shí)文檔已經(jīng)說(shuō)得很直白了:
Note that null keys (i.e. entry.get()* == null 如果 key threadlocal 為 null 了,這個(gè) entry 就可以清除了。
ThreadLocal是一個(gè)弱引用,當(dāng)為null時(shí),會(huì)被當(dāng)成垃圾回收 。
造成內(nèi)存泄露的原因在于ThreadLocal為null,也就是要被垃圾回收器回收了,但是此時(shí)我們的ThreadLocalMap(thread 的內(nèi)部屬性)生命周期和Thread的一樣,它不會(huì)回收,這時(shí)候就出現(xiàn)了一個(gè)現(xiàn)象。那就是ThreadLocalMap的key沒(méi)了,但是value還在,這就造成了內(nèi)存泄漏。
再詳細(xì)點(diǎn)來(lái)說(shuō),ThreadLocal在沒(méi)有外部強(qiáng)引用時(shí),發(fā)生GC時(shí)會(huì)被回收,如果創(chuàng)建ThreadLocal的線程一直持續(xù)運(yùn)行,那么這個(gè)Entry對(duì)象中的value就有可能一直得不到回收,發(fā)生內(nèi)存泄露。
就比如線程池里面的線程,線程都是復(fù)用的,那么之前的線程實(shí)例處理完之后,出于復(fù)用的目的線程依然存活,所以,ThreadLocal設(shè)定的value值被持有,導(dǎo)致內(nèi)存泄露。
按照道理一個(gè)線程使用完,ThreadLocalMap是應(yīng)該要被清空的,但是現(xiàn)在線程被復(fù)用了。
解決辦法:
每次在使用完ThreadLocal的時(shí)候一定要remove。
為什么ThreadLocal要使用弱引用?
如果使用強(qiáng)引用,當(dāng)ThreadLocal 對(duì)象的引用(強(qiáng)引用)被回收了,ThreadLocalMap本身依然還持有ThreadLocal的強(qiáng)引用,如果沒(méi)有手動(dòng)刪除這個(gè)key ,則ThreadLocal不會(huì)被回收,所以只要當(dāng)前線程不消亡,ThreadLocalMap引用的那些對(duì)象就不會(huì)被回收, 可以認(rèn)為這導(dǎo)致Entry內(nèi)存泄漏。
- 強(qiáng)引用:普通的引用,強(qiáng)引用指向的對(duì)象不會(huì)被回收。
- 軟引用:僅有軟引用指向的對(duì)象,只有發(fā)生gc且內(nèi)存不足,才會(huì)被回收。
- 弱引用:僅有弱引用指向的對(duì)象,只要發(fā)生gc就會(huì)被回收。