ThreadLoacal是什么斤蔓?
ThreadLocal是啥?以前面試別人時(shí)就喜歡問這個(gè)培愁,有些伙伴喜歡把它和線程同步機(jī)制混為一談监氢,事實(shí)上ThreadLocal與線程同步無關(guān)。ThreadLocal雖然提供了一種解決多線程環(huán)境下成員變量的問題衅码,但是它并不是解決多線程共享變量的問題拯刁。那么ThreadLocal到底是什么呢?
API是這樣介紹它的:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
該類提供了線程局部 (thread-local) 變量逝段。這些變量不同于它們的普通對(duì)應(yīng)物垛玻,因?yàn)樵L問某個(gè)變量(通過其
get
或set
方法)的每個(gè)線程都有自己的局部變量,它獨(dú)立于變量的初始化副本奶躯。ThreadLocal
實(shí)例通常是類中的 private static 字段帚桩,它們希望將狀態(tài)與某一個(gè)線程(例如,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)嘹黔。
所以ThreadLocal與線程同步機(jī)制不同账嚎,線程同步機(jī)制是多個(gè)線程共享同一個(gè)變量,而ThreadLocal是為每一個(gè)線程創(chuàng)建一個(gè)單獨(dú)的變量副本儡蔓,故而每個(gè)線程都可以獨(dú)立地改變自己所擁有的變量副本郭蕉,而不會(huì)影響其他線程所對(duì)應(yīng)的副本∥菇可以說ThreadLocal為多線程環(huán)境下變量問題提供了另外一種解決思路召锈。
ThreadLocal定義了四個(gè)方法:
- get():返回此線程局部變量的當(dāng)前線程副本中的值。
- initialValue():返回此線程局部變量的當(dāng)前線程的“初始值”开呐。
- remove():移除此線程局部變量當(dāng)前線程的值烟勋。
- set(T value):將此線程局部變量的當(dāng)前線程副本中的值設(shè)置為指定值规求。
除了這四個(gè)方法,ThreadLocal內(nèi)部還有一個(gè)靜態(tài)內(nèi)部類ThreadLocalMap卵惦,該內(nèi)部類才是實(shí)現(xiàn)線程隔離機(jī)制的關(guān)鍵阻肿,get()、set()沮尿、remove()都是基于該內(nèi)部類操作丛塌。ThreadLocalMap提供了一種用鍵值對(duì)方式存儲(chǔ)每一個(gè)線程的變量副本的方法,key為當(dāng)前ThreadLocal對(duì)象畜疾,value則是對(duì)應(yīng)線程的變量副本赴邻。
對(duì)于ThreadLocal需要注意的有兩點(diǎn):
1. ThreadLocal實(shí)例本身是不存儲(chǔ)值,它只是提供了一個(gè)在當(dāng)前線程中找到副本值得key啡捶。
2. 是ThreadLocal包含在Thread中姥敛,而不是Thread包含在ThreadLocal中,有些小伙伴會(huì)弄錯(cuò)他們的關(guān)系瞎暑。
下圖是Thread彤敛、ThreadLocal、ThreadLocalMap的關(guān)系(http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)
ThreadLocal使用示例
示例如下:
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
// 實(shí)現(xiàn)initialValue()
public Integer initialValue() {
return 0;
}
};
public int nextSeq(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public static void main(String[] args){
SeqCount seqCount = new SeqCount();
SeqThread thread1 = new SeqThread(seqCount);
SeqThread thread2 = new SeqThread(seqCount);
SeqThread thread3 = new SeqThread(seqCount);
SeqThread thread4 = new SeqThread(seqCount);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
private static class SeqThread extends Thread{
private SeqCount seqCount;
SeqThread(SeqCount seqCount){
this.seqCount = seqCount;
}
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
}
}
}
}
運(yùn)行結(jié)果:
從運(yùn)行結(jié)果可以看出了赌,ThreadLocal確實(shí)是可以達(dá)到線程隔離機(jī)制墨榄,確保變量的安全性。這里我們想一個(gè)問題勿她,在上面的代碼中ThreadLocal的initialValue()方法返回的是0袄秩,加入該方法返回得是一個(gè)對(duì)象呢,會(huì)產(chǎn)生什么后果呢逢并?例如:
A a = new A();
private static ThreadLocal<A> seqCount = new ThreadLocal<A>(){
// 實(shí)現(xiàn)initialValue()
public A initialValue() {
return a;
}
};
class A{
// ....
}
具體過程請參考:對(duì)ThreadLocal實(shí)現(xiàn)原理的一點(diǎn)思考
ThreadLocal源碼解析
ThreadLocal雖然解決了這個(gè)多線程變量的復(fù)雜問題之剧,但是它的源碼實(shí)現(xiàn)卻是比較簡單的。ThreadLocalMap是實(shí)現(xiàn)ThreadLocal的關(guān)鍵砍聊,我們先從它入手猪狈。
ThreadLocalMap
ThreadLocalMap其內(nèi)部利用Entry來實(shí)現(xiàn)key-value的存儲(chǔ),如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
從上面代碼中可以看出Entry的key就是ThreadLocal辩恼,而value就是值。同時(shí)谓形,Entry也繼承WeakReference灶伊,所以說Entry所對(duì)應(yīng)key(ThreadLocal實(shí)例)的引用為一個(gè)弱引用(關(guān)于弱引用這里就不多說了,感興趣的可以關(guān)注這篇博客:Java 理論與實(shí)踐: 用弱引用堵住內(nèi)存泄漏)
ThreadLocalMap的源碼稍微多了點(diǎn)寒跳,我們就看兩個(gè)最核心的方法getEntry()聘萨、set(ThreadLocal> key, Object value)方法。
set(ThreadLocal> key, Object value)
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根據(jù) ThreadLocal 的散列值童太,查找對(duì)應(yīng)元素在數(shù)組中的位置
int i = key.threadLocalHashCode & (len-1);
// 采用“線性探測法”米辐,尋找合適位置
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在胸完,直接覆蓋
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因?yàn)榇颂幍膃 != null)翘贮,說明之前的ThreadLocal對(duì)象已經(jīng)被回收了
if (k == null) {
// 用新元素替換陳舊的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal對(duì)應(yīng)的key實(shí)例不存在也沒有陳舊元素赊窥,new 一個(gè)
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清楚陳舊的Entry(key == null)
// 如果沒有清理陳舊的 Entry 并且數(shù)組中的元素大于了閾值,則進(jìn)行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這個(gè)set()操作和我們在集合了解的put()方式有點(diǎn)兒不一樣狸页,雖然他們都是key-value結(jié)構(gòu)锨能,不同在于他們解決散列沖突的方式不同。集合Map的put()采用的是拉鏈法芍耘,而ThreadLocalMap的set()則是采用開放定址法(具體請參考散列沖突處理系列博客)址遇。掌握了開放地址法該方法就一目了然了。
set()操作除了存儲(chǔ)元素外斋竞,還有一個(gè)很重要的作用倔约,就是replaceStaleEntry()和cleanSomeSlots(),這兩個(gè)方法可以清除掉key == null 的實(shí)例坝初,防止內(nèi)存泄漏浸剩。在set()方法中還有一個(gè)變量很重要:threadLocalHashCode,定義如下:
private final int threadLocalHashCode = nextHashCode();
從名字上面我們可以看出threadLocalHashCode應(yīng)該是ThreadLocal的散列值脖卖,定義為final乒省,表示ThreadLocal一旦創(chuàng)建其散列值就已經(jīng)確定了,生成過程則是調(diào)用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一個(gè)ThreadLocal實(shí)例的threadLocalHashCode的值畦木,HASH_INCREMENT則表示分配兩個(gè)ThradLocal實(shí)例的threadLocalHashCode的增量袖扛,從nextHashCode就可以看出他們的定義。
getEntry()
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);
}
由于采用了開放定址法十籍,所以當(dāng)前key的散列值和元素在數(shù)組的索引并不是完全對(duì)應(yīng)的蛆封,首先取一個(gè)探測數(shù)(key的散列值),如果所對(duì)應(yīng)的key就是我們所要找的元素勾栗,則返回惨篱,否則調(diào)用getEntryAfterMiss(),如下:
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;
}
這里有一個(gè)重要的地方围俘,當(dāng)key == null時(shí)砸讳,調(diào)用了expungeStaleEntry()方法,該方法用于處理key == null界牡,有利于GC回收簿寂,能夠有效地避免內(nèi)存泄漏。
get()
返回當(dāng)前線程所對(duì)應(yīng)的線程變量
public T get() {
// 獲取當(dāng)前線程
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程的成員變量 threadLocal
ThreadLocalMap map = getMap(t);
if (map != null) {
// 從當(dāng)前線程的ThreadLocalMap獲取相對(duì)應(yīng)的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 獲取目標(biāo)值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
首先通過當(dāng)前線程獲取所對(duì)應(yīng)的成員變量ThreadLocalMap宿亡,然后通過ThreadLocalMap獲取當(dāng)前ThreadLocal的Entry常遂,最后通過所獲取的Entry獲取目標(biāo)值result。
getMap()方法可以獲取當(dāng)前線程所對(duì)應(yīng)的ThreadLocalMap挽荠,如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set(T value)
設(shè)置當(dāng)前線程的線程局部變量的值克胳。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
獲取當(dāng)前線程所對(duì)應(yīng)的ThreadLocalMap平绩,如果不為空,則調(diào)用ThreadLocalMap的set()方法漠另,key就是當(dāng)前ThreadLocal捏雌,如果不存在,則調(diào)用createMap()方法新建一個(gè)酗钞,如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
initialValue()
返回該線程局部變量的初始值腹忽。
protected T initialValue() {
return null;
}
該方法定義為protected級(jí)別且返回為null,很明顯是要子類實(shí)現(xiàn)它的砚作,所以我們在使用ThreadLocal的時(shí)候一般都應(yīng)該覆蓋該方法窘奏。該方法不能顯示調(diào)用,只有在第一次調(diào)用get()或者set()方法時(shí)才會(huì)被執(zhí)行葫录,并且僅執(zhí)行1次着裹。
remove()
將當(dāng)前線程局部變量的值刪除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
該方法的目的是減少內(nèi)存的占用米同。當(dāng)然骇扇,我們不需要顯示調(diào)用該方法,因?yàn)橐粋€(gè)線程結(jié)束后面粮,它所對(duì)應(yīng)的局部變量就會(huì)被垃圾回收少孝。
ThreadLocal為什么會(huì)內(nèi)存泄漏
前面提到每個(gè)Thread都有一個(gè)ThreadLocal.ThreadLocalMap的map,該map的key為ThreadLocal實(shí)例熬苍,它為一個(gè)弱引用稍走,我們知道弱引用有利于GC回收。當(dāng)ThreadLocal的key == null時(shí)柴底,GC就會(huì)回收這部分空間婿脸,但是value卻不一定能夠被回收,因?yàn)樗€與Current Thread存在一個(gè)強(qiáng)引用關(guān)系柄驻,如下(圖片來自http://www.reibang.com/p/ee8c9dccc953):
由于存在這個(gè)強(qiáng)引用關(guān)系狐树,會(huì)導(dǎo)致value無法回收。如果這個(gè)線程對(duì)象不會(huì)銷毀那么這個(gè)強(qiáng)引用關(guān)系則會(huì)一直存在鸿脓,就會(huì)出現(xiàn)內(nèi)存泄漏情況抑钟。所以說只要這個(gè)線程對(duì)象能夠及時(shí)被GC回收,就不會(huì)出現(xiàn)內(nèi)存泄漏野哭。如果碰到線程池味赃,那就更坑了。
那么要怎么避免這個(gè)問題呢虐拓?
在前面提過,在ThreadLocalMap中的setEntry()傲武、getEntry()蓉驹,如果遇到key == null的情況城榛,會(huì)對(duì)value設(shè)置為null。當(dāng)然我們也可以顯示調(diào)用ThreadLocal的remove()方法進(jìn)行處理态兴。
下面再對(duì)ThreadLocal進(jìn)行簡單的總結(jié):
- ThreadLocal 不是用于解決共享變量的問題的狠持,也不是為了協(xié)調(diào)線程同步而存在,而是為了方便每個(gè)線程處理自己的狀態(tài)而引入的一個(gè)機(jī)制瞻润。這點(diǎn)至關(guān)重要喘垂。
- 每個(gè)Thread內(nèi)部都有一個(gè)ThreadLocal.ThreadLocalMap類型的成員變量,該成員變量用來存儲(chǔ)實(shí)際的ThreadLocal變量副本绍撞。
- ThreadLocal并不是為線程保存對(duì)象的副本正勒,它僅僅只起到一個(gè)索引的作用。它的主要目的是為每一個(gè)線程隔離一個(gè)類的實(shí)例傻铣,這個(gè)實(shí)例的作用范圍僅限于線程內(nèi)部章贞。