dcddc

西米大人的博客

0%

系统学习threadLocal

功能

提供本地线程变量。放在threadLocal的内容只能被当前线程看到

实现

简单理解就是每个Thread都有一个ThreadLocalMap变量,这个变量里维护了threadLocal对象和value的映射关系,threadLocal的get/set方法,其实都是操作的当前线程的threadLocalMap变量

ThreadLocalMap本质是一个Entry数组,看下它的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
...
...

每个entry维护了一个threadLocal对象和value的映射关系,Entry有两点需要说明:

  • entry对象是一个指向threadLocal对象的弱引用,还负责维护value值
  • entry数组的下标与threadLocal对象的hashCode值强绑定

第一点从Entry类定义就能知道,第二点看一下threadLocal的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
26
private void set(ThreadLocal<?> key, Object value) {
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();
}

threadlocal的set方法:
构建一个弱引用entry,指向threadLocal对象,value为set的值,丢到当前线程threadLocalMap的entry数组里,下标基于threadLocal的hashCode计算得到

threadlocal的get方法简单总结如下,不贴代码了:
拿到当前线程的threadLocalMap,根据threadLocal的hash值取出entry数组里对应的entry,再拿到value

threadlocal的OOM问题

先说一个结论:如果只有弱引用指向这个对象,在下一次gc时,这个对象会被gc!
当threadLocal对象只被弱引用entry对象引用时,下一次gc就会回收threadlocal对象。当threadLocal被gc后,指向它的弱引用entry对象并不会被gc,因为thread-->threadLocalMap-->entry[]这个引用链都是强引用。但entry.value永远无法被访问,为什么呢?
前面说过,entry数组的下标基于threadLocal对象的hashCode计算得到,既然threadLocal被gc,那么这个entry对象就无法被访问,entry.value也就无法被访问。直到thread执行结束被gc时,entry对象才会被gc。如果内存中有很多这种entry对象,会存在OOM的风险。

思考:
为什么entry要用弱引用指向threadLocal?
很简单,因为只有entry用弱引用,当threadLocal被gc时,我们才能感知到,然后把entry也gc!否则,entry用强引用,只有线程退出了,entry才会被gc,更容易OOM!用软引用也不行,软引用只有当OOM时,才会将不在其他地方被引用的threadLocal给gc了,这被感知到时已经为时已晚了!

解决办法:
jvm会在每次调用threadLocal.set\get\remove方法时,会把这些entry(弱引用指向的threadlocal被回收)对象gc,这里不去深究,不过好的习惯是在finally块里手动调用threadLocal.remove来主动触发无效entry的gc

InheritableThreadLocal

简单理解它就是一种父子线程可共享的threadLocal
Thread除了threadLocals维护threadLocal,还有一个inheritableThreadLocals,维护可被子线程共享的threadLocal

1
2
3
4
5
6
7
8
9
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

如果线程使用InheritableThreadLocal,调用InheritableThreadLocal的createMap方法时会维护到thread.inheritableThreadLocals

1
2
3
4
5
6
7
8
9
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

...
...

void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

需要注意的是,只有在子线程创建的时候,构造函数里会去父线程的inheritableThreadLocals拷贝一份到自己的inheritableThreadLocals,之后父线程set的inheritableThreadLocal值不会被子线程获取

1
2
3
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);