结构介绍:由上图可知,一条线程 Thread 包含一个 ThreadLocalMap,这个Map里面包含许多这条线程存储的局部变量值,而获取这些线程局部变量的 key 就是众多的自定义 ThreadLocal 对象的弱引用。简单概括,ThreadLocalMap 是存储在线程 Thread 里面的一个成员属性,ThreadLocal 中的内部类 ThreadLocalMap 则拥有操作这个 ThreadLocalMap 的 API ,由下面的代码可以看出来 。
说明:该源码来自于 jdk_1.8.0_162 版本。
Thread 中相关的代码结构
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
ThreadLocal 代码结构
public class ThreadLocal {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
//这个魔数的选取与 斐波那契散列 以及 黄金分割数 有关
//当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {...}
protected T initialValue() {...}
public static ThreadLocal withInitial(Supplier extends S> supplier) {...}
public ThreadLocal() {}
public T get() {...}
private T setInitialValue() {...}
public void set(T value) {...}
public void remove() {...}
ThreadLocalMap getMap(Thread t) {...}
void createMap(Thread t, T firstValue) {...}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {...}
T childValue(T parentValue) {...}
static final class SuppliedThreadLocal extends ThreadLocal {...}
static class ThreadLocalMap {...}
}
我们先来看看最常用的 get 和 set 方法:
// get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
关键在于这几行:
//获取当前线程的ThradLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//根据ThradLocal获取对应的线程局部变量
ThreadLocalMap.Entry e = map.getEntry(this);
//将当前ThradLocal作为key向ThreadLocalMap存储线程局部变量
map.set(this, value);
说明:在调用 get 或者 set 方法时,首选会去获取到当前的线程对象,然后通过这个线程对象获取到当前线程维护的 ThreadLocalMap,再把当前的 ThreadLocal 对象(也就是this)作为 key 到 ThradLocalMap 进行获取或者存储操作,另外获取到的 thread 不能自定义,并且当前的 ThreadLocal 对象,也就是this也不能自定义,这个做法也就保证了无论什么方法调用 get、set 或者 remove,都只会操作的是当前线程的 ThreadLocalMap。
static class ThreadLocalMap {
//Entry继承了弱引用类WeakReference,而ThreadLocal>是被弱引用的对象,所以Entry的key是使用弱引用的方式
static class Entry extends WeakReference> {
// 往ThreadLocal里实际塞入的值
Object value;
//Entry的key是使用弱引用的方式
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
//初始容量,必须为2的幂的数
private static final int INITIAL_CAPACITY = 16;
//Entry数组,其大小为2的幂的数
private Entry[] table;
//数组里entry的个数
private int size = 0;
//重新分配table大小的阈值,默认为0
private int threshold;
... ...
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();
//如果已经存在与当前key相同的Entry则覆盖其value
if (k == key) {
e.value = value;
return;
}
//如果存在key为null的Entry,无论旧的value是否为null,都将新的value设置进去,并且根据条件清理一些无用的垃圾value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
... ...
}
说明:这里面有一个比较有意思的地方,那就是在set方法中的 for循环 使用到的下一个元素下标的获取方式,也就是这个方法调用:
e = tab[i = nextIndex(i, len)]
// nextIndex 方法实现:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
结论:当 i + 1 >= len 时,也就是超过 Map 中数组下标时,又回到了首个数组的位置,所以这里我们可以推导出来 ThreadLocalMap 中 Entry 数组的结构是一个环形的,后面会有详解 。
说明:由上图可以看出,ThreadLocalMap 里面的 Entry 使用一个环形的结构,这是为什么呢,因为 ThreadLocalMap 插入元素遇到哈希冲突时,使用的是线性探查法来处理冲突 (HashMap 使用的是拉链法),这也就导致了他是个环形。到这里可能我们又会生出新的疑问,什么是线性探测法?没关系,接下来我们好好了解一下线性探测法。
线性探测法(停车图借鉴而来)
简介:如上图,直接使用数组来存储数据。可以想象成一个停车问题。若当前车位已经有车,则你就继续往前开,直到找到下一个为空的车位 。
实现步骤
(1) 得到要插入元素的 key 。
(2) 通过哈希方法计算得到元素 key 的 hashCode,通过 hashCode 使用取模或者其他方式计算出该元素的位置 i 。
(3) 定位到位置 i,与该位置的元素对比,若不冲突,则直接填入数组 。
(4) 若冲突,则使 i++,也就是往后找,直到找到第一个 data[i] 为空的情况,则填入 。若到了尾部可循环到前面,正是因为这个,所以数组形成了一个环形 。
强引用:通过new创建的对象,即使OOM都不会回收。
软引用:软引用可达的对象在内存不充足时才会被回收。
弱引用:无论内存是否充足,进行GC时均会对弱引用对象进行回收。
虚引用:用作记录它指向的对象已被回收,这个虚引用只是个记录作用,甚至不能通过get获取到对应的对象。
回到开头的那张图片:
解析:左边的栈里面持有对右边 ThreadLocal 对象的强引用,而右边堆里面的 ThreadLocal对象 和 key 之间是弱引用的关系,正常情况下,栈依旧持有 ThreadLocal 的强引用,因为 ThreadLocal 被强引用在引用着,所以不会被回收,而当调用方在使用完这个局部变量 value 后,将左边栈里面 ThreadLocal 的对象引用设置为 null,这个时候,堆里面的ThreadLocal 对象已经没有任何强引用引用着他了,只有右边的 key 对他保持着弱引用,但这个弱引用不会影响垃圾收集器对 ThreadLocal 对象的回收,当下一次垃圾收集器进行垃圾回收时就会将 ThreadLocal 对象回收掉。所以这个弱引用的好处有以下几点:
(1) 在当前线程未结束前,将 ThreadLocal 何时回收的操作权交给调用方,使用完毕后将 ThreadLocal 的引用设置为 null 即可。
(2) 当 ThreadLocal 对象被回收后,key 的指向就变为了 null,便于后面对 value 的回收(这个下面说) 。
value的回收
上面说到了 ThreadLocal 被回收后 key 就指向了 null,但是此时 Value 依旧有这一条强引用:Thread对象引用 -- Thread对象 -- ThreadLocalMap -- Entry -- Value 。在当前线程没结束前,该 Value 不会被垃圾收集器回收,但是这个 Value 对应的 Key 已经变为了 null,也就是说此时的 Value 已经变成了一个不能通过 Key 访问的对象垃圾,这种问题往往在使用线程池的情境下尤为突出,甚至有引起 OOM 的风险 。对于这个不可用的 Value 对象,ThreadLocal 中的内部类 ThreadLocalMap 里面有专门的方法来对它进行回收,方法如下:
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) {
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;
}
解析:这个方法将目标位置上的 value 设置为 null 后,继续检查其他位置上是否还有 key 为 null 的 value,将 value 也全部设置为 null,以便GC时回收内存空间。
那么通过哪些方法可以触发这个 expungeStaleEntry 清理方法呢,通过对 ThreadLocal 源码的阅读,最终发现可以触发该方法的操作有 get、set、remove 操作。所以,特别是在使用线程池的情况下,在使用完 ThreadLocal 后,一定要手动调用 remove 方法释放内存资源,remove 方法直接将该 Entry 所占的空间全部释放掉,如果不释放有引起 OOM 的风险。