看大神的代码偶然发现代码中的ThreadLocal,一脸不解
让我们先看下应用代码:只有一个threadlocal实例,一个get方法,一个set方法,一个销毁的方法
private static final ThreadLocal<FootTracerInfo> traceInfo = new ThreadLocal<FootTracerInfo>(); public static FootTracerInfo getTraceInfo() { return traceInfo.get(); } public static void setTraceInfo(FootTracerInfo httpClientInfo) { traceInfo.set(httpClientInfo); } public static void destroyTraceInfo() { traceInfo.remove(); }
看来这似乎是一个容器,跟踪下get方法,发现这似乎和线程有关,一时激动非常,看来是处理并发环境下使用的
ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }
接下来,看一下google大神的解释吧(节约下时间)
综合看到的文章,总结一下,threadlocal作为一个静态实例可以为美式整脊疗法每个线程存储一个泛型对象(FootTracerInfo)只对本线程使用,其他线程不可访问。恩,大概是明白ThreadLocal类的用处了。
那么现在我们细细探究下ThreadLocal是如何实现的呢?
我们上面跟踪源码get方法发现,它获取了当前线程,并以此去获取了一个ThreadLocalMap对象,原来这是Thread里面的一个成员变量
|
1
|
ThreadLocal.ThreadLocalMap
threadLocals = null;
|
同时我们看看此变量是如果存储的,key是this也就是threadlocal对象实例(traceInfo),value就是我们要存储的FootTracerInfo对象,现在它就只能被自身线程所持有了
|
1
2
3
|
void createMap(Thread
t, T firstValue) {
t.threadLocals
= new ThreadLocalMap(this,
firstValue);
}
|
也就是说每个线程都有一个独属于它的ThreadLocalMap实例,这样是不是说就解决了变量在线程间不可见
似乎是完事大吉了!!
等等,似乎网友有人说我们可能会用线程池呢,那么之前的线程实例处理完之后出于复用的目的依然存活 ,那么它所持有的值是不是会泄露呢。
我们知道线程池的主要目的是重用存在的线程,减少对象创建、销毁的开销,这样线程并不会在处理完任务时关闭。
|
1
2
3
4
5
6
7
8
|
public void set(T
value) {
Thread
t = Thread.currentThread();
ThreadLocalMap
map = getMap(t);
if (map
!= null)
map.set(this,
value);
else
createMap(t,
value);
}
|
让我们看下set的源码,因为线程并未关闭,所以getMap获取的ThreadLocalMap可能并不为空,而this是静态的ThreadLocal的实例,最终此线程存储的值将会被新的value所覆盖,这时以前的value就不再有强引用指向它,因此将会在下次gc时候回收掉。
当然,我们看到代码中remove方法,看下源码先,也就是说我们本线程的任务结束时显式调用一下remove方法,将会把当前线程下的map中存储的key,value删除,这样即使使用线程池时,线程未关闭而是重复利用,也不会出现存储的值泄露问题。
|
1
2
3
4
5
|
public void remove()
{
ThreadLocalMap
m = getMap(Thread.currentThread());
if (m
!= null)
m.remove(this);
}
|
现在我们再来探究下ThreadLocalMap中的弱引用问题吧
如下图:
我们再来看下ThreadLocalMap的源码,发现这里的key使用了一个对ThreadLocal的弱引用
|
1
2
3
4
5
6
7
8
9
|
static class Entry extends WeakReference<ThreadLocal>
{
/**
The value associated with this ThreadLocal. */
Object
value;
Entry(ThreadLocal
k, Object v) {
super(k);
value
= v;
}
}
|
现在我们就探究下这里弱引用的作用,看了许多大神的说法,大概说来:
1 当key即ThreadLocal对象被置为null时,弱引用不会影响gc对ThreadLocal对象的回收;
2 当一个线程持有多个ThreadLocal实例时,部分实例可能出现不再有强引用而被gc回收,从而导致map中的key为null,value因强引用而不会被gc回收,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄露。但是如果thread运行结束,整个线程对象被回收,那么value所引用的对象也就会被垃圾回收。
什么情况下 ThreadLocal对象会被回收了,典型的就是ThreadLocal对象作为局部对象来使用或者每次使用的时候都new了一个对象。所以一般情况下,ThreadLocal对象都是static的,确保不会被垃圾回收以及任何时候线程都能够访问到这个对象。
那么再来看看ThreadLocalMap的设计中是如何处理的:
看看源码,我们在set和get方法中都发现了相似的处理,即擦除key为null位置的entry
set方法中:
/** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ 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; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } 以及replaceStaleEntry方法中 // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value);
get方法中:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
ThreadLocal
k = e.get();
if (k
== key)
return e;
if (k
== null)
expungeStaleEntry(i);
expungeStaleEntry
方法:
tab[staleSlot].value
= null;
tab[staleSlot]
= null;
size--;
|
除此之外,在源码的rehash方法中,我们看到再判断扩容前先调用了expungeStaleEntries函数,其对map做了一次遍历,对于key为null的entry执行了expungeStaleEntry函数,也即是清除了key为null的entry,那么什么时候调用rehash呢,请看上面set方法的源码
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
*
Re-pack and/or re-size the table. First scan the entire
*
table removing stale entries. If this doesn't sufficiently
*
shrink the size of the table, double the table size.
*/
private void rehash()
{
expungeStaleEntries();
//
Use lower threshold for doubling to avoid hysteresis
if (size
>= threshold - threshold / 4)
resize();
}
/**
*
Expunge all stale entries in the table.
*/
private void expungeStaleEntries()
{
Entry[]
tab = table;
int len
= tab.length;
for (int j
= 0;
j < len; j++) {
Entry
e = tab[j];
if (e
!= null &&
e.get() == null)
expungeStaleEntry(j);
}
}
|
可见设计者也注意到这个地方可能出现内存泄露,为了防止这种情况发生, 做了如上处理。
本文参考了一些大神的文章,现在贴下链接:
1 http://www.cnblogs.com/xzwblog/p/7227509.html
2 http://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/index.html
3 http://droidyue.com/blog/2014/10/12/understanding-weakreference-in-java/
4 http://www.cnblogs.com/onlywujun/p/3524675.html
5 http://blog.csdn.net/huachao1001/article/details/51734973
可能还有一些查过没有记录现在也找不到了,抱歉!!
免责声明:本文章和信息来源于国际互联网,本网转载出于传递更多信息和学习之目的。如转载稿涉及版权等问题,请立即联系。我们会予以更改或删除相关文章,保证您的权利。