在JDK中,有些不起眼的类,往往蕴含着巨大的能量,ThreadLocal就是这样一个类,JDK1.2该类就诞生了,可算做JDK的一个元老了。从本篇开始,楼主打算分三篇讲解下对ThreadLocal的认知,预计会从源码分析到开源项目ThreadLocal的应用,以及ThreadLocal最有价值的分布式链路跟踪来逐步展示ThreadLocal的魅力!以下为文章标题,先立好Flag后填坑。
分析ThreadLocal
,绕不开Thread
、ThreadLocalMap
、ThreadLocalMap.Entry
这个铁三角,理清这三者层次关系,是突破ThreadLocal的关键。一图胜千言,上图!
Thread
对象都持有一个threadLocals属性,其类型为 ThreadLocalMap
,它是ThreadLocal
的一个静态内部类;ThreadLocalMap
内部维护一个ThreadLocaMap.Entry[]
数组,其中Entry的key为ThreadLocal
类型,value是客户端设置的对象;ThreadLocal
对象,这个引用比较特殊,采取的是弱引用 WeakReference
弱引用(WeakReference): 弱引用关联的对象,当GC发生时,无论内存够不够,弱引用指向的对象都会被回收。
Q:为什么Thread中要持有ThreadLocalMap这样一个map结构?
A:这样设计是为了能让线程有能力存储多个ThreadLocal对象,如下应用场景所示:
private static ThreadLocal threadLocal_int = new ThreadLocal<>();
private static ThreadLocal threadLocal_string = new ThreadLocal<>();
// threadLocal_int 和 threadLocal_int 具有不同的 threadLocalHashCode,故123和abc存放在同一个线程的ThreadLocalMap的2个Entry中
public void test() {
threadLocal_int.set(123);
threadLocal_string.set("abc");
}
判断自己有没有掌握ThreadLocal,2个问题就可以检验出来:
问题先不回答,带着这2个疑问我们进行分析,从分析中推导出答案,这样印象才能更深刻!
换个角度思考,如果是强引用,会有什么问题?对于ThreadLocal,持有其引用的来源有两个:A.客户端持有的ThreadLocal_ref ,这个引用客户端是能操作赋值的;B.线程内部持有的ThreadLocalMap.Entry的key过来的引用,这个引用客户端完全不感知;假设客户端主动释放对ThreadLocal的引用(如:通过赋值ThreadLoca_ref= null;
),但由于还存在B来源的强引用,那ThreadLocal这个对象就一直无法被GC回收,故ThreadLocal对象存在内存泄漏风险。
结论:强引用存在内存泄漏风险
上面分析了,强引用确实存在内存泄漏的可能性,那弱引用是不是就不存在内存泄漏风险? 来一副图接着分析: 当客户端主动释放对ThreadLocal对象的强引用(通过赋值ThreadLoca_ref = null;
),GC发生时,由于Entry.key对ThreadLocal是弱引用,故ThreadLocal对象会被回收掉; 但是,Entry的value还是保持着对Object的强引用,由于Entry.key已经是null了,客户端已经没有任何方式能定位到这个Entry,故Entry的value对象存在内存泄漏风险。
结论:弱引用也存在内存泄漏风险
PS:
1、在实际应用ThreadLocal时,几乎不会人为断开对ThreadLocal的引用。JDK给出的建议也是使用 static 修饰 ThreadLocal,这样就会一直保持着ThreadLocal的一个强引用;
2、ThreadLocalMap的get、set、remove方法都有考虑到Entry.key=null的情况。这三个操作执行时会顺带清除Entry.key=null的Entry.value (源代码里面有体现),这样大大降低了内存泄漏的发生率;
3、无论强、弱引用都存在内存泄漏的风险,为什么设计上还选择弱引用呢?原因很简单,弱引用相对强引用出现内存泄漏的概率更低一点,毕竟还能回收ThreadLocal腾点内存空间出来!
之前的分析,都是分析客户端断开对ThreadLocal的引用。如果是客户端断开对Thread对象的强引用Thread_ref(通过赋值Thread_ref=null;
),显然GC发生时,Thread对象由于没有被引用肯定会被干掉,ThreadLocalMap本身又是Thread对象的属性,二者同生共死。同理,顺带着ThreadLocalMap、ThreadLocal.Entry及ThreadLocal都会被GC干掉,整个堆内存干净了。但是,Thread算是一类比较稀缺的资源,实际应用往往采用线程池的来循环利用线程,故让客户端断开Thread对象的强引用来避免内存泄漏理论上可行但不符合实践。
再回顾下上面两种内存泄漏场景:强引用ThreadLocal时,内存泄漏的原因在Entry.key持有ThreadLocal的强引用,导致ThreadLocal无法被GC回收;在弱引用ThreadLocal时,内存泄漏发生在Entry.key=null的情况下,Entry的value没有途径可以被释放;显然这两种情况问题都出在Entry这个对象上,直接干掉Entry不就没内存泄漏了吗?是的,JDK的设计大师们自然想到这点,因此提供了ThreadLocal.remove()方法来干这事。
综上:避免内存泄漏的最佳实践就是当用完ThreadLocal后,主动触发ThreadLocal.remove()来清除整个Entry对象,采用如下样板代码。
ThreadLocal.set(value);
try {
// 这里是业务代码
} finally {
ThreadLocal.remove();
}
对照着第一幅图,再看ThreadLocal应该不费力了。ThreadLocal常用方法有如下4个:
// 交给给客户端进行覆写自定义初始值的生成
protected T initialValue() {
return null;
}
public void set(T value) {
// 获得当前线程
Thread t = Thread.currentThread();
// 获得当前线程持有的 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 调用ThreadLocalMap 的set方法;这里的this就是当前触发set方法的ThreadLocal对象本身
map.set(this, value);
else
// 直接 new ThreadLocalMap(this, value) 并赋值给 t.threadLocals
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
// threadLocals 就是线程持有的 ThreadLocal.ThreadLocalMap 类型变量
return t.threadLocals;
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 调用ThreadLocalMap 的getEntry方法
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 触发初始化initialValue方法,顺便把set方法的逻辑走一遍,最后返回初始值
return setInitialValue();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
// 初始值
T value = initialValue();
// 下面的内容跟set方法完全一样;这才体现出 setInitialValue 这个方法名的含义!
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
// 返回初始值
return value;
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 调用 ThreadLocalMap的remove方法
m.remove(this);
}
单看ThreadLocal上面的四个方法,其实还是比较清晰的。可以把ThreadLocal看做一个门面类,没有过多的逻辑,真正比较重的逻辑都委托给 ThreadLocalMap
来做了。
ThreadLocalMap才是真正干活的,对应 ThreadLocal的四个方法,也提供了相应的几个方法:set() -> ThreadLocalMap.set()、get() -> getEntry(ThreadLocal> key)、remove() -> remove(ThreadLocal> key),源码如下:
ThreadLocalMap.set(ThreadLocal> key , Object value)
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);
// 如果ThreadLocal对应的key找得到,则进行赋值
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) {
// Entry值为null, 则进行清理
replaceStaleEntry(key, value, i);
return;
}
}
// 如果ThreadLocal对应的key找不到,则新建Entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 顺带做点脏数据清理工作,内部触发 expungeStaleEntry
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 清除key=null的Entry,显示将Entry.value=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) {
// 清除key=null的Entry的value
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;
}
Entry getEntry(ThreadLocal> key)
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
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
// 什么情况下会进这里来?发生了GC,由于是弱引用,Entry的key指向的ThreadLocal对象已经被GC回收了,但Entry的value还没被清理
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
// 拿到key指向的ThreadLocal对象
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// key指向的ThreadLocal对象为null,说明ThreadLocal对象被垃圾回收了,故需要清理掉Entry的value
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove(ThreadLocal> key)
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
// 清除脏数据
expungeStaleEntry(i);
return;
}
}
}
最后,再上一幅图,总结下ThreadLocal的set、get、remove三个方法串起来涉及到的对象和调用链路。
结论:
全文终~