文章結(jié)構(gòu)如下:
簡介
ThreadLocal是為了解決線程安全而產(chǎn)生的航唆。它解決線程安全的思路不同于synchronized:使多個線程對于共享資源的訪問串行化,只有一個線程能夠獲取到對象鎖院刁,其他線程進入同步隊列等待糯钙。也不同于volatile,通過lock指令生成內(nèi)存屏障來使得其他線程訪問變量時需要從主內(nèi)存加載最新值退腥,在線程寫入值時能夠立刻刷新到主內(nèi)存任岸,但是volatile不能保證原子性,因此使用時具有一定局限阅虫。ThreadLocal的解決思路是線程封閉演闭,那么無論線程什么時候,在哪個方法里面訪問ThreadLocal變量颓帝,都只會訪問到自己線程的ThreadLocal值米碰。避免了將參數(shù)通過方法進行傳遞,也無需擔(dān)心其他線程會訪問到本線程的變量值购城。
源碼理解
我們經(jīng)常使用的api是ThreadLocal的get()吕座,set(),remove()方法瘪板,通過get方法切入吴趴,可以發(fā)現(xiàn)對于每個Java線程,都維護了一個ThreadLocalMap侮攀。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap
想要讀懂源碼锣枝,就繞不開對于ThreadLocalMap的理解,ThreadLocalMap本質(zhì)結(jié)構(gòu)跟HashMap差不多兰英,只不過Entry的組成不同撇叁,解決沖突的方式不同。
-
數(shù)據(jù)結(jié)構(gòu)
通過Thread獲取到ThreadLocalMap畦贸,然后每個ThreadLocalMap的Entry存儲著ThreadLocal對象與value的對象關(guān)系陨闹,當(dāng)設(shè)置了多個ThreadLocal變量時楞捂,對于每一個ThreadLocal對象,會在每一個ThreadLocalMap中保存一份趋厉。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
- 為什么Entry中的ThreadLocal對象是弱引用寨闹?
在Java中,定義了四種引用:
- 強引用:Object obj = new Object()君账,對于這種引用繁堡,除非顯示將obj=null,否則虛擬機不會將其回收
- 軟引用:用來描述一些還有用乡数,但非必須的對象帖蔓。在系統(tǒng)將要內(nèi)存溢出之前,會把軟引用對象列入回收范圍進行第二次回收瞳脓。
- 弱引用:引用強度比軟引用更弱塑娇,只能生存到下一次虛擬機垃圾回收之前。
-
虛引用
我們在代碼中實例化的對象引用是保存在虛擬機棧上劫侧,和Entry的key引用同一個對象埋酬,之所以要將Entry的key設(shè)置為弱引用的原因就是如果我們將外部引用設(shè)置為null,那么ThreadLocal的實例不再有強引用烧栋,只有弱引用写妥,在下次虛擬機進行垃圾回收時就可以將其回收了。但是依然還存在內(nèi)存泄漏問題审姓,因為Entry不會被回收珍特。
- ThreadLocalMap解決沖突的方式
解決Hash沖突的方式主要有拉鏈法、開放地址法魔吐、二次hash法扎筒、建立公共溢出區(qū)。ThreadLocalMap解決沖突的方式是開放地址法酬姆,如果通過Hash函數(shù)算出下標(biāo)已經(jīng)存儲過Entry了嗜桌,它會線性環(huán)形搜索沒有被使用的位置。我理解使用線性檢測的原因是只要線程中的ThreadLocal對象不多辞色,那么根據(jù)擴容因子算出的ThreadLocalMap的數(shù)組大小也不會很大骨宠,所以即使退化到最差的o(n),對性能的影響也不大相满,而且這種線性搜索相比鏈?zhǔn)椒绞蕉愿庸?jié)省空間层亿。
對于線性探測的結(jié)點增刪、擴容可以參考:線性探測解決Hash沖突
內(nèi)存泄漏
上面說到ThreadLocal設(shè)置為弱引用是為了防止內(nèi)存泄漏立美,所謂內(nèi)存泄漏就是指堆中已經(jīng)不再使用的對象沒有被回收匿又,造成空間 的浪費,而且積累下去很可能會造成內(nèi)存溢出悯辙。當(dāng)Entry中的key被回收時琳省,整個Entry就沒有用了,但是由于value還持有虛擬機棧上的強引用躲撰,所以不會被回收针贬,這樣就還是會造成內(nèi)存泄漏。但是ThreadLocal中的get()拢蛋、set()桦他、remove()方法都會調(diào)用replaceStaleEntry、cleanSomeSlots谆棱、expungeStaleEntry方法進行回收
-
清理方法:cleanSomeSlots
下標(biāo)i用來控制訪問的范圍快压,如果沒有找到key為null的Entry,那么會遍歷log2(n)垃瞧,i的下標(biāo)環(huán)形遞增蔫劣。如果找到一個key不為null的位置,n會置為len个从,相當(dāng)于是增大了范圍
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
- expungeStaleEntry的清除邏輯
cleanSomeSlots函數(shù)脉幢,在key為null的結(jié)點進入expungeStaleEntry方法,將當(dāng)前槽位的value和Entry都設(shè)為null嗦锐,并且還繼續(xù)往下環(huán)形搜索嫌松,一直到table[i]為null才退出,搜索過程中奕污,遇到key為null的結(jié)點就進行清除萎羔,如果key不為null,就對結(jié)點進行rehash碳默,rehash的目的就是為了讓結(jié)點離hash函數(shù)的下標(biāo)更近贾陷,這樣查找的時候就不會在線性搜索浪費時間了。remove和get時嘱根,都會調(diào)用該方法進行清理昵宇。
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;
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;
}
- replaceStaleEntry方法
這個方法在set()過程,當(dāng)key為null時調(diào)用儿子。從i開始首先前環(huán)向搜索臟 entry瓦哎,一直到table[i]=null結(jié)束。然后從下標(biāo)i開始向后搜索柔逼,如果有key相同的就覆蓋蒋譬,并和臟entry交換。根據(jù)不同情況愉适,設(shè)置cleanSomeSlots清除節(jié)點的范圍
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//向前找到第一個key為null的entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
////如果在向后環(huán)形查找過程中發(fā)現(xiàn)key相同的entry就覆蓋并且和臟entry進行交換
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果在查找過程中還未發(fā)現(xiàn)臟entry犯助,那么就以當(dāng)前位置作為cleanSomeSlots
//的起點
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果向前未搜索到臟entry,則在查找過程遇到臟entry的話维咸,后面就以此時這個位置
//作為起點執(zhí)行cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//如果在查找過程中沒有找到可以覆蓋的entry剂买,則將新的entry插入在臟entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
對于下面這個例子惠爽,插入的位置是4,從下標(biāo)4向前搜索到3就停止瞬哼,更新slotExpunge為3.再從4向后遍歷尋找可覆蓋的entry婚肆,當(dāng)前例子未找到,于是以slotExpunge為下標(biāo)調(diào)用cleanExpunge清理臟entry坐慰。
最佳實踐
使用場景
- 數(shù)據(jù)庫連接
Hibernate的數(shù)據(jù)庫連接池就是將connection放進threadlocal實現(xiàn)的 - 用戶Session等信息
- 對請求的requestBody较性,requestUrl等進行處理
代碼中ThreadLocal修飾了requestBody變量,因為服務(wù)器使用的是Tomcat结胀,所以一個請求會交給一個線程來處理赞咙,那么requestBody的get()和set方法設(shè)置的就是當(dāng)前線程的請求體的值,跟其他線程互不影響糟港。
public class ReqLogInterceptor implements HandlerInterceptor {
ThreadLocal<String> requestBody = new ThreadLocal<String>();
@Override
public boolean preHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, Object o) throws Exception {
requestBody.set("");
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView)
throws Exception {
if (httpServletResponse instanceof ContentCachingResponseWrapper) {
responseBody.set("");
byte[] body = ((ContentCachingResponseWrapper) httpServletResponse).getContentAsByteArray();
responseBody.set(new String(body, httpServletResponse.getCharacterEncoding()));
}
}
}
及時remove
及時調(diào)用ThreadLocal的remove方法攀操,可以避免內(nèi)存泄漏問題,更重要的是防止造成業(yè)務(wù)邏輯的錯亂秸抚,因為通常會使用線程池管理線程崔赌,如果一個用戶登錄之后的name相關(guān)的ThreadLocal對象,沒有及時remove耸别,那么其他用戶登錄進來之后健芭,會發(fā)現(xiàn)自己的用戶名顯示錯誤。