ThreadLocal源码分析

前言


ThreadLocal类作为线程相关的比较重要的类,在开发和面试中经常被用到或者问到。比如之前我们实现的分布式锁中,在基于ZK的分布式锁中就有被用到。当时它的作用是记录下在ZK中当前锁的目录和前一个锁的目录。从而达到有序释放和获取锁的目的。

简单介绍

ThreadLocal能保证其中的数据只属于当前线程,而不能被其他线程所访问,但是一个ThreadLocal变量只能保存一份的数据,比如

1
private ThreadLocal<String> curPath = new ThreadLocal<>(); // 当前锁的目录

像这种保存了一个K和V,但是一个线程其实可以创建多个ThreadLocal对象,来保存不同的数据,所以在Thread类中有一个threadLocals的成员变量来保存所有的threadlocal。它是一个ThreadLocalMap的变量,后续再做源码分析。所以我们可以通过这个map来保存和获取本线程中所有的threadlocal

1
ThreadLocal.ThreadLocalMap threadLocals = null;

源码分析

以下分析基于jdk1.8

ThreadLocalMap结构

ThreadLocalMap类似于一个HashMap的结构,保存了当前线程中所有threadlocalKV信息。它是ThreadLocal中的一个静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final int INITIAL_CAPACITY = 16;

private void setThreshold(int len) {
threshold = len * 2 / 3;
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

从构造函数可以看出,ThreadLocalMap是一个Entry类型的数组,其中初始容量INITIAL_CAPACITY=16,阈值为长度的2/3。

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;
}
}

其中Map中的每一个实体Entry是继承了弱引用的,这边关注到它的key对象,key是一个ThreadLocal类型的对象,也就是ThreadLocal把自己当成了一个key存入Map中。并且我们可以看到,这个Map是没有next指针的,也就是说它无法像HashMap一样用链地址法解决哈希冲突,另外,由于Entry是继承弱引用的,所以这边的key是一个弱引用。也就是说,只要key==null的时候,这个key即被回收。
总结一下,这里可能会产生两个问题。一个是ThreadLocalMap是如何解决哈希冲突的,另一个是当key被回收掉之后,value其实还是存在的,那么如何解决value的问题。

给出ThreadLocalMap中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
27
28
29
30
31
32
33
34
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;
// 找到key对应的位置i
int i = key.threadLocalHashCode & (len-1);
// 可以看出解决哈希冲突的方法是开放地址方法,也就是若当前Entry不为空,则继续往下找,直到找到一个为空的Entry为止。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果threadlocal要设置的key等于当前位置i的k,就直接覆盖掉原来的value
if (k == key) {
e.value = value;
return;
}
// 如果当前位置没有键值(为空),则调用replaceStaleEntry函数,这个函数主要是为了清除之前提到的弱引用key值失效,value还存在的问题。并且把新值放在该位置上。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 执行到这边说明还没有找到,直接new一个Entry并放在对应的位置上
tab[i] = new Entry(key, value);
int sz = ++size;
// 超过阈值则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

总结一下:有三种情况存在。1.若位置i上面已经有一个key值并且等于我现在要新set的key,则直接覆盖掉。2.当前Entry不为空,但是key为空,则调用replaceStaleEntry()删除掉前后某个范围内key为空的不空Entry。并将新值存入。3.若没有找到相同key的Entry或者没有key为空的Entry,也就是找到了一个null,那么就直接把新值放进去。

内存泄露问题

之前提到ThreadLocalMap中的key是一个弱引用,也就是当key==null的时候一旦发生GC就会被回收。如果ThreadLocal一直持续运行,那么这个Entry对象的value就一直得不到回收,发生内存泄露的问题。那该如何避免呢?在调用ThreadLocal的get()和set()方法的时候,会清除key值为null的对象Entry。也就是replaceStaleEntry()这个方法,简单来看一下里面的逻辑。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

// 当Entry不为空,但是Entry的key为空的时候会进到这个方法中。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

int slotToExpunge = staleSlot;
// 从当前位置往前找,直到找到某个Entry为null的位置为止。
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
// slotToExpunge 记录staleSlot左手边第一个空的entry 到staleSlot 之间key过期最小的index
if (e.get() == null)
slotToExpunge = i;

// 和前面的过程相反,从当前位置往后找,直到找到某个Entry为null的位置为止。
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();

// 往后找的过程中如果找到了一个key等于要放入的key值的Entry,则交换到staleSlot这个位置,也就是刚开始进来的那个Entry的key为空需要被回收的位置。如果不交换会可能出现两个相同的key的问题。
if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// expungeStaleEntry()函数是让后面的元素往前移,因为如果不往前移的话可能会出现永远访问不到的问题。cleanSomeSlots()函数是清除对象的。
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// 如果key 在数组中没有存在,那么直接新建一个新的放进去就可以
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// 如果有其他已经过期的对象,那么需要清理他
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

正是在ThreadLocal的get和set方法中都有replaceStaleEntry()函数的调用,所以正常情况下是不存在内存溢出的问题的,但是如果我们没有调用get和set的时候就有可能面临内存溢出的问题,所以一般来说在不再使用的时候调用remove()方法可以加快垃圾回收,避免内存溢出。

疑问:当没有强引用指向ThreadLocal中的ThreadLocalMap,即它被回收掉之后,不也没有内存溢出的问题了吗?

但是有一种危险是,如果线程是线程池的, 在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap 和里面的元素是不会回收掉的。

ThreadLocal中的方法

上面提到了ThreadLocal中的set和get方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

public T get() {
Thread t = Thread.currentThread();
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;
}
}
return setInitialValue();
}

其他问题

ThreadLocalMap 采用开放地址法原因

  1. ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table,关于这个神奇的数字google 有很多解析,这里就不重复说了
  2. ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低

如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?

InheritableThreadLocal解决父子线程传递Threadlcoal值的问题。

1.在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量

2.在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去

3.因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值

参考:

https://juejin.im/post/5d8b2bde51882509372faa7c

https://zhuanlan.zhihu.com/p/28501035