看大神的代码偶然发现代码中的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里面的一个成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
同时我们看看此变量是如果存储的,key是this也就是threadlocal对象实例(traceInfo),value就是我们要存储的FootTracerInfo对象,现在它就只能被自身线程所持有了
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
也就是说每个线程都有一个独属于它的ThreadLocalMap实例,这样是不是说就解决了变量在线程间不可见
似乎是完事大吉了!!
等等,似乎网友有人说我们可能会用线程池呢,那么之前的线程实例处理完之后出于复用的目的依然存活 ,那么它所持有的值是不是会泄露呢。
我们知道线程池的主要目的是重用存在的线程,减少对象创建、销毁的开销,这样线程并不会在处理完任务时关闭。
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删除,这样即使使用线程池时,线程未关闭而是重复利用,也不会出现存储的值泄露问题。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
现在我们再来探究下ThreadLocalMap中的弱引用问题吧
如下图:
我们再来看下ThreadLocalMap的源码,发现这里的key使用了一个对ThreadLocal的弱引用
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方法中:
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方法的源码
/** * 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
可能还有一些查过没有记录现在也找不到了,抱歉!!