前言
上一篇我们已经粗略讲解了 ThreadLocal 的常见问题和原理。接下来要逐字逐句地去解析源码了。上篇还有很多没有讲清楚的这次也慢慢补上。至于应用我周末会把这几天写的文章好好在斟酌一下,感觉最近有点追求数量而没有追求质量。毕竟我有点心急。好了,废话不多说,进入源码学习吧!
源码分析
其实 ThreadLocal 源码没什么东西,都是调用 ThreadLocalMap 的。
hash 相关
ThreadLocalMap 元素的哈希值
// threadLocal 会存到 Thread.threadLocals 的 map中,
// 该 hashcode 经过计算后可以确定要存放的桶位
private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
* 创建ThreadLocal对象时会使用到,每创建一个threadLocal对象 就会使用nextHashCode
* 分配一个hash值给这个对象。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
/* 每创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。
* 这个值很特殊,它是斐波那契数也叫黄金分割数。hash增量为这个数字,带来的好处就是 hash分布非常均匀。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
* 创建新的ThreadLocal对象时 会给当前对象分配一个hash,使用这个方法。
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
get 方法
接下来解析 get 方法,解析 get 分为两块,一块是 ThreadLocal 的 get 方法,一块是 ThreadLocalMap 的 get 方法。而 ThreadLocal.get() 调用的是 ThreadLocalMap.getEntry()。那么首先看 ThreadLocal.get()。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程 Thread 对象的 threadLocals map 引用
ThreadLocalMap map = getMap(t);
// 条件成立:说明当前线程已经拥有自己的 ThreadLocalMap 对象了
if (map != null) {
// key:当前 threadLocal 对象
// 调用 map.getEntry() 方法,获取 threadLocalMap 中该 threadLocal 关联的 entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 条件成立:说明当前线程初始化过
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
/*
* 执行到这里有几种情况?
* 1. 当前线程对应的 threadLocalMap 是空
* 2. 当前线程与当前 threadLocal 对象没有生成过相关联的 线程局部变量
*/
return setInitialValue();
}
// 如果当前 threadLocal 没有 value,初始化一个
// 如果当前线程没有 threadLocalMap 的话,还会初始化创建map。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 将 thread 于 value 关联
map.set(this, value);
else
//执行到这里,说明 当前线程内部还未初始化 threadLocalMap ,这里调用createMap 给当前线程创建map
createMap(t, value);
return value;
}
// 大部分会重写的
protected T initialValue() {
return null;
}
// 就是给当前线程的 threadLocals 初始化
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这里记住一个小点,ThreadLocal 第一次初始化的时候是 get 方法。
接下来看一下核心源码 getEntry()。ThreadLocal 对象 get() 操作其实是由 ThreadLocalMap.getEntry 代理完成的。当 要查找的桶位上的元素为空或者 桶位的 key 不是我们要查找元素的 key,那么我们会调用 getEntryAfterMiss 方法向后继续查找。为什么会这么设计呢?因为 ThreadLocalMap 发生冲突时不会产生链表,如果当前散列表 slot 上有元素,就会向后查找直到找到可以使用的位置。
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
//1. e == null
//2. e.key != 要查找的 key
/**
* getEntryAfterMiss 方法会继续向当前桶位后面继续搜索 e.key = key 的entry
*/
return getEntryAfterMiss(key, i, e);
}
/**
* key:threadlocal 对象
* i: key 计算出来的 index
* e: table[index] 中的 entry
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 循环处理当前元素,碰到 slot == null 情况,搜索结束
while (e != null) {
// 获取 slot 中 entry 对象 的key
ThreadLocal<?> k = e.get();
if (k == key)
// 找到了
return e;
// 条件成立:说明当前 slot 中的 entry#key 关联的 threadLocal 对象已经被 GC 回收了
// 因为 key 是弱引用 key = entry.get = null
if (k == null)
// 做一次探测式过期数据回收
expungeStaleEntry(i);
else
// 更新 index,继续向后搜索
i = nextIndex(i, len);
// 获取下一个 slot 中的 entry
e = tab[i];
}
// 没找到
return null;
}
//参数 staleSlot table[staleSlot] 就是一个过期数据,以这个元素开始,向后查找过期数据
//碰到 slot == null 的情况结束
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// help GC
tab[staleSlot].value = null;
// 这里直接置空
tab[staleSlot] = null;
// 干掉一个元素 -1
size--;
// 清理完get 过程中的过期数据还不算完,它还会继续向后查找,如果继续碰倒过期数据也会清理掉
// 知道碰倒 slot == null 为止
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;//for 直到碰倒 slot == null 结束
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 条件成立:说明 k 表示的 theadlocal 对象已经被 GC 回收了..当前 entry 属于脏数据
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 执行到这里说明当前 slot 中对应的 entry 是非过期数据
// 前面有可能清理掉了几个过期数据
// 当前元素有可能碰倒 hash 冲突了,往后偏移存储了,那么重新计算位置,让位置更靠近正确位置
// 这样查询效率更高
// 重新计算 entry 对应的 index
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;
}
set 方法
修改当前线程与当前 threadLocal 对象相关联的 线程局部变量。(源码很简单,几乎在上面都看过。)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
我们着重看一下 map 的 set()。
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)]) {
// 获取当前元素的 key
ThreadLocal<?> k = e.get();
// 如果要插入的 key 和 桶位的 key 相等,说明这是一个替换操作
if (k == key) {
e.value = value;
return;
}
// 条件成立:说明遇到了过期数据
if (k == null) {
// 替换逻辑
replaceStaleEntry(key, value, i);
return;
}
}
//执行到这里,碰到了 slot == null
// 直接插入就可以了
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里单独分开是因为替换过期的方法 replaceStaleEntry() 很难。它大致的原理是如果该 slot 上有过期数据,那么我们就强制将值设置到该桶位。
// staleSlot: 上层方法 set 方法,迭代查询时 发现这个 slot 是一个过期 entry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 表示开始探测式清理过期数据的开始下标。默认从 staleSlot 开始
int slotToExpunge = staleSlot;
// 向前迭代,找到没有过期的数据过期数据,循环遇到 null 结束
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
// 找到过期数据,探测式清理数据开始下标更新为 i
if (e.get() == null)
slotToExpunge = i;
// 以当前 staleSlot 开始向后查找,直到找到 null 位置
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//条件成立,说明替换逻辑
// 这里是什么意思呢?就是如果我计算好某个下标比如 4 是应该插入的位置,
// 但是 4 号位是过期数据。那么我们向后查找的过程中发现 6 号位与 4 号位 key 一样
// 那么可能 6 号插入时发生冲突了。所以我们要把要插入的元素放到它应该存在的位置。
// 即 4 号和 6 号互换位置。
if (k == key) {
e.value = value;
// 交换位置
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 向后查找过程中并未发现 k == key 的 entry,说明当前 set 操作是一个添加逻辑
// 直接新数据添加到 table[staleSlot]对应的 slot 中
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 条件成立:除了当前 staleSlot 以外,还发现其他过期 slot,所以要开启清理数据逻辑
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
rehash
rehash 也是一个难点,代表了 ThreadLocalMap 的扩容逻辑。ThreadLocalMap 的扩容不是到达阈值之后直接进行扩容,而是到达阈值后去尝试清理数据。如果当前散列表中元素还是超过 四分之三的阈值,那么再进行扩容。扩大到源来容量的两倍。
private void rehash() {
// 这个方法执行完后,当前散列表内的所有过期数据,都会被清理
expungeStaleEntries();
// 清理完之后,散列表内的 entry 数量仍达到了 threshould * 3/4 真正触发扩容
if (size >= threshold - threshold / 4)
resize();
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 扩容后新表的大小
int newLen = oldLen * 2;
// 创建出扩容后的散列表
Entry[] newTab = new Entry[newLen];
//新表中的元素数量
int count = 0;
// 遍历老表,迁移数据到新表
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
// 条件成立:老表中指定位置有数据
if (e != null) {
ThreadLocal<?> k = e.get();
// 说明老表中当前位置是一个过期数据
if (k == null) {
e.value = null; // Help the GC
} else {
// 执行到这里说明这里是非过期数据,需要迁移到扩容后的表、
int h = k.threadLocalHashCode & (newLen - 1);
// 循环找到一个距离 h 最近的一个可以使用的 slot
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 将数据存放在新的合适的位置
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
结语
整个 ThreadLocal 系列就算告一段落,周末会在第一篇文字补点应用。原理内容什么的都将清楚了,感觉再面试中遇到不会被问倒吧。希望对您有帮助。
2021年03月17日22:41:14
站在巨人肩膀
B 站小刘讲源码