基本原理&使用場(chǎng)景
在多線程的并發(fā)訪問(wèn)的場(chǎng)景,除了使用鎖來(lái)控制不同線程對(duì)臨界區(qū)的訪問(wèn),來(lái)避免競(jìng)爭(zhēng),還有另外一種方式,就是ThreadLocal.
ThreadLocal中持有的數(shù)據(jù)只有當(dāng)前線程可以訪問(wèn),其他線程訪問(wèn)不了,這樣就避免了線程競(jìng)爭(zhēng)。
基本使用
下面是一個(gè)簡(jiǎn)單的threadLocal使用的例子,展示了ThreadLocal在多線程場(chǎng)景中的基本使用。
public class ThreadLocalDemo {
private static final ThreadLocal<Integer> threadLocalFruit = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocalVegetable = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(new ThreadLocalTestThread("農(nóng)人A", 3, 5)).start();
new Thread(new ThreadLocalTestThread("農(nóng)人B", 7, 9)).start();
LockSupport.park();
}
static void say(String words, Object... params) {
System.out.println(MessageFormat.format(words, params));
}
static class ThreadLocalTestThread implements Runnable {
private String name;
private int fruitNum;
private int vegetableNum;
private Random random = new Random();
public ThreadLocalTestThread(String name, int fruitNum, int vegetableNum) {
this.name = name;
this.fruitNum = fruitNum;
this.vegetableNum = vegetableNum;
}
@Override
public void run() {
while (true) {
say("{0} 收獲 {1} 個(gè)水果", name, fruitNum);
threadLocalFruit.set(fruitNum);
say("{0} 收獲 {1} 個(gè)蔬菜", name, vegetableNum);
threadLocalVegetable.set(vegetableNum);
try {
TimeUnit.MICROSECONDS.sleep(random.nextInt(500));
} catch (InterruptedException e) {
e.printStackTrace();
}
say("{0} 用掉 {1} 個(gè)水果", name, threadLocalFruit.get());
say("{0} 用掉 {1} 個(gè)蔬菜", name, threadLocalVegetable.get());
}
}
}
}
程序的執(zhí)行結(jié)果如下,從執(zhí)行結(jié)果來(lái)看,【農(nóng)人1】線程對(duì)threadlocal的set僅【農(nóng)人1】可見,【農(nóng)人2】線程對(duì)threadlocal的set僅【農(nóng)人2】可見,兩者對(duì)threadloacl的get/set互不影響。
農(nóng)人B 收獲 7 個(gè)水果
農(nóng)人A 收獲 3 個(gè)水果
農(nóng)人B 收獲 9 個(gè)蔬菜
農(nóng)人A 收獲 5 個(gè)蔬菜
農(nóng)人B 用掉 7 個(gè)水果
農(nóng)人B 用掉 9 個(gè)蔬菜
農(nóng)人A 用掉 3 個(gè)水果
農(nóng)人B 收獲 7 個(gè)水果
農(nóng)人A 用掉 5 個(gè)蔬菜
農(nóng)人B 收獲 9 個(gè)蔬菜
農(nóng)人A 收獲 3 個(gè)水果
農(nóng)人A 收獲 5 個(gè)蔬菜
農(nóng)人B 用掉 7 個(gè)水果
農(nóng)人B 用掉 9 個(gè)蔬菜
農(nóng)人A 用掉 3 個(gè)水果
農(nóng)人B 收獲 7 個(gè)水果
農(nóng)人A 用掉 5 個(gè)蔬菜
農(nóng)人B 收獲 9 個(gè)蔬菜
農(nóng)人A 收獲 3 個(gè)水果
農(nóng)人A 收獲 5 個(gè)蔬菜
原理介紹
這么神奇的特性是怎么做到的呢?先看ThreadLocal的get方法源碼.
從get方法的源代碼來(lái)看,從threadlocal獲取數(shù)據(jù)的時(shí)候,是從當(dāng)前線程對(duì)象的ThreadLocalMap中獲取的,所以獲取到的數(shù)據(jù)線程鎖特有的,不會(huì)被其他線程修改。
public T get() {
Thread t = Thread.currentThread();
//每個(gè)線程都有自己的ThreadLocalMap
//ThreadLocalMap保存threadlocal中的數(shù)據(jù)
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;
}
}
//如果當(dāng)前線程的ThreadLocalMap為空,則初始化ThreadLocalMap,
//并將initialValue方法的返回值放入到ThreadLocalMap中。
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
從下面的代碼來(lái)看, ThreadLocalMap的key是一個(gè)弱引用,弱應(yīng)用關(guān)聯(lián)的對(duì)象內(nèi)存在jvm gc的時(shí)候會(huì)被回收掉,可以避免key導(dǎo)致的內(nèi)存泄露的問(wèn)題,但value仍然是強(qiáng)引用,當(dāng)key被回收之后,value就獲取不到了,可能導(dǎo)致內(nèi)存泄露。既然這樣,為什么key要使用弱引用呢?詳見【ThreadLocal內(nèi)存泄露風(fēng)險(xiǎn)分析】
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal內(nèi)存泄露風(fēng)險(xiǎn)分析
上面提到,既然弱引用的key被回收掉之后會(huì)導(dǎo)致value的內(nèi)存泄露,那為什么key仍然要使用弱應(yīng)用呢?當(dāng)ThreadLocal對(duì)象沒有強(qiáng)應(yīng)用時(shí),它們需要被清理掉,如果key是強(qiáng)引用,當(dāng)引用threadLocal的對(duì)象都被回收掉時(shí),因?yàn)閗ey是強(qiáng)引用,還指向threadLocal,導(dǎo)致threadlocal無(wú)法被回收掉。從下面的引用關(guān)系圖能比較直觀的看到這個(gè)問(wèn)題。

既然可能會(huì)造成內(nèi)存泄露,那怎么解決這個(gè)問(wèn)題呢。ThreadLocal給出的解決方案是在get/set的時(shí)候,會(huì)調(diào)用expungeStaleEntry去清理key=null的value和entry。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
//這里k為null,則執(zhí)行value、entry的清理動(dòng)作
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
數(shù)據(jù)結(jié)構(gòu)&算法
ThreadLocal的核心數(shù)據(jù)結(jié)構(gòu)就是ThreadLocalMap,核心的代碼如下:
這里會(huì)即將k為null的value也設(shè)置為null,然后將entry也設(shè)置為null,這樣gc就可以回收掉了。然后對(duì)于k不為null的entry使用了高德斯算法(俗稱洗牌算法)來(lái)重排,保證table中的元素隨機(jī)分布,盡量避免hash沖突。
此外,從set方法源碼也知,ThreadLocalMap 解決hash沖突使用的是開放地址法,就是一直在數(shù)組中尋找null的位置放入當(dāng)前數(shù)據(jù)。解決hash沖突另一個(gè)比較有名的算法是鏈?zhǔn)降刂贩?,HashMap就是經(jīng)典案例,只不過(guò)java8之后,hashMap做了改進(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);
}
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;
}
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;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
其他
1、為避免threadlocal造成的內(nèi)存泄露,要在最后調(diào)用remove方法。
2、正常情況,當(dāng)Thread執(zhí)行完會(huì)被銷毀,Thread指向的threadLocalMap就會(huì)變?yōu)槔?,里面的entry也就會(huì)被回收掉。
3、發(fā)生內(nèi)存泄露的場(chǎng)景一般是在線程池的場(chǎng)景,當(dāng)線程場(chǎng)景存活,key是弱引用被回收掉之后,key變成null,value的值還在,就可能出現(xiàn)內(nèi)存泄露
4、在線程池的場(chǎng)景,如果在最后沒有做remove,還可能會(huì)導(dǎo)致ThreadLocal里的數(shù)據(jù)被二次改寫,造成多線程修改同個(gè)ThreadLocal的假象,其實(shí)是相同的線程對(duì)象被復(fù)用后,二次修改原線程對(duì)象ThreadLocal數(shù)據(jù)的問(wèn)題。