ThreadLocal类可以理解为线程本地变量。每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。通过让每个线程有自己的独立副本从而实现线程封闭的机制。每个线程有一个自己的ThreadLocalMap。底层是一个数组实现的。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
存储结构解析
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这个Entry的key是一个弱引用。value是真正的值。为什么需要用弱引用:弱引用在每次GC的时候都会回收,且不会影响其对象的回收。即一个对象只剩弱引用,那么他会被回收。如果使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
ThreadLocal的set方法:首先根据线程找到map,然后根据设置值。如果map不存在就创建一个。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap的set方法:
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 获取到数组的下标
int i = key.threadLocalHashCode & (len-1);
// 如果下标有值 发生了hash冲突 使用线性探测
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到了key 直接覆盖
if (k == key) {
e.value = value;
return;
}
// 这个hash值下有对象过期了 进行替换
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到空地方了 插入
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry方法:
前向搜索,出现key为null的Entry肯定是因为上次GC了,而之所以去前向搜索,是因为很有可能其它Entry在上次GC中也没能存活。遍历到为数组元素为null。
向后遍历,是因为ThreadLocal用的是开地址,很可能当前的stale entry对应的并不是hascode为此槽索引的Entry,而是因为哈希冲突后移的Entry,那么很有可能hascode对应该槽的Entry会往后排。遍历到为数组元素为null。
每次发现了过期数据会调用cleanSomeSlots进行清理
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
ThreadLocal.ThreadLocalMap.Entry e;
// 向前移动 看是否有过期的entry 将最前一个过期的下标赋值为slotToExpunge
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 向后寻找
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 找到了 进行交换
// 这种情况: staleSlot这个位置的数据已经过期 而i>=staleSlot
// 根据探测方式 这个hash值应该先放staleSlot 后放i 所以需要交换
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 前面的位置没有过期的 那么过期的就从i开始
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清理数据 从slotToExpunge 到 len
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 前面没有过期的 且这个位置过期了(k == null) 清理开始位置赋值为i
// slotToExpunge代表发现的第一个数据过期的位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 没有找到这个key 先赋值
tab[staleSlot].value = null;
tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
// 发现有过期的数据 进行清理和调整
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
expungeStaleEntry:负责将第一个过期元素的位置到元素为null的位置中间的元素重新hash,移动到应有的位置,释放掉key为null的元素。
private int expungeStaleEntry(int staleSlot) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// staleSlot位置数据已经过期 设置为null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 调整
// 因为使用的是线性探测方法 所以需要把因发生了hash冲突的entry向前移动
ThreadLocal.ThreadLocalMap.Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 数据过期 设置为null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 检测是不是因为hash冲突而移动的值
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // 是因为hash冲突而移动的值
tab[i] = null;
// h是本应该放的位置 然后使用线下探测找到第一个空位
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// staleSlot后面第一个为null的位置
return i;
}
expungeStaleEntry(int staleSlot):此时staleSlot就等于1。按照逻辑,会把2 3 5这三个元素重新hash,会把1 4释放.
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
key | 10 | null | 20 | 23 | null | 24 | 28 | |||
value | v0 | v1 | v2 | v3 | v4 | v5 | v8 |
就会变成。此时返回值为5.第一个null的坐标。此时v1和v4才能被gc回收
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
key | 10 | 20 | 23 | 24 | 28 | |||||
value | v0 | v2 | v3 | v5 | v8 |
看一下cleanSomeSlots方法。就是调用log2(n)次expungeStaleEntry方法,是查询和回收的一个折中。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);// log2(n)
return removed;
}