我朋友cute轩前几天面试,正好阿里爸爸看他读过JUC包下源码,直接提起面试官小哥哥的兴趣,直接死磕ThreadLocal源码,面完试已经汗流浃背了,犹如一场高手对决,辛亏他看完源码,才勉强应战。今天就好好分析ThreadLocal源码。
ThreadLocal是JUC包下提供的,它提供了本地变量,也就是让每个线程都有自己的独立空间来存储变量,且该变量不会受到其他线程的影响,也可以理解为每个线程都可以在自己的独立空间中操作变量,不会影响到其他线程中的变量值。
如上类图可知Thread类中有一个threadLocals和inheritableThreadLocals都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认每个线程中这个两个变量都为null,只有当前线程第一次调用了ThreadLocal的set或者get方法时候才会进行创建。
ThreadLocalMap采用延迟加载策略,至少存放一个k-v值才会创建
// threadLocalMap 内部散列表数组引用,数组的长度 必须是 2的次方数
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap内部采用Entry对象封装键值对,并且将key值设置为弱引用,便于其回收
弱引用: 使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取到当前线程Thread对象的 threadLocals map引用
ThreadLocalMap map = getMap(t);
// 代表当前线程 拥有自己的threadLocal map 对象
if (map != null) {
// 获取 threadLocalMap 中 该threadLocal关联的 entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 说明当前线程 初始化过 与当前threadLocal对象相关联的 线程局部变量
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 执行到这里有几种情况?
// 1.当前线程对应的threadLocalMap是空
// 2.当前线程与当前threadLocal对象没有生成过相关联的 线程局部变量..
// setInitialValue方法初始化当前线程与当前threadLocal对象 相关联的value
// 且 当前线程如果没有threadLocalMap的话 还会初始化创建map
return setInitialValue();
}
get()方法流程:
private T setInitialValue() {
// 重写
T value = initialValue();
Thread t = Thread.currentThread();
// 获取当前线程和threadLocal相关联的 threadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null)
//保存当前threadLocal与当前线程生成的 线程局部变量。
//key: 当前threadLocal对象 value:线程与当前threadLocal相关的局部变量
map.set(this, value);
else
//执行到这里,说明 当前线程内部还未初始化 threadLocalMap, 这里调用createMap 给当前线程创建map
//参数1:当前线程 参数2:线程与当前threadLocal相关的局部变量
createMap(t, value);
return value;
}
// 创建ThreadLocalMap对象
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
setInitialValue流程:
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)]) {
// 获取value值
ThreadLocal<?> k = e.get();
// 条件成立 说明当前set操作是一个替换嘈杂哦
if (k == key) {
e.value = value;
return;
}
// 如果为空,表示该slot已被回收,需要进行探测式回收
if (k == null) {
// 探测式回收
replaceStaleEntry(key, value, i);
return;
}
}
// 找到可利用的桶位 赋值
tab[i] = new Entry(key, value);
int sz = ++size;
// 检查是否需要进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap.set() 流程
// slot 指当前位置为空的桶位
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
// 从当前的桶为上一个位置开始遍历,因为当前桶位数据为空
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 说明找到比当前桶位更靠前的桶位值 用slotToExpunge记录
// Find either the key or trailing null slot of run, whichever
// occurs first
// 从从前桶位向后遍历 直到碰到null为止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// //条件成立:说明咱们是一个 替换逻辑
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;
// 进行启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 条件成立 说明向前遍历未找到过期的桶位,将当前桶位赋值到slotToExpunge
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 执行到这里 说明当前set操作 是一个添加逻辑..
// 将数据添加到当前staleSlot中
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//条件成立:除了当前staleSlot 以外 ,还发现其它的过期slot了.. 所以要开启 清理数据的逻辑..
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
ThreadLocalMap.replaceStaleEntry() 分析
private void rehash() {
// 清理掉当前散列表内的所有过期的数据
expungeStaleEntries();
// 当前散列表长度是否大于阈值*0.75
if (size >= threshold - threshold / 4)
// 扩容操作
resize();
}
rehash()方法 过程
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];
// 条件成立 说明当前桶位值未被GC回收
if (e != null) {
// 获取value值
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
// 重新计算在新散列表中的桶位
int h = k.threadLocalHashCode & (newLen - 1);
// 解决Hash冲突 向后遍历 直到遇到空桶位
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 进行赋值
newTab[h] = e;
count++;
}
}
}
// 设置阈值 = 散列表长度的2/3
setThreshold(newLen);
size = count;
table = newTab;
}
resize()方法 过程:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的threadLocalMap对象
ThreadLocalMap map = getMap(t);
//条件成立:说明当前线程的threadLocalMap已经初始化过了
if (map != null)
//调用threadLocalMap.set方法 进行重写 或者 添加。
map.set(this, value);
else
//执行到这里,说明当前线程还未创建 threadLocalMap对象。
//参数1:当前线程 参数2:线程与当前threadLocal相关的局部变量
createMap(t, value);
}
ThreadLocal#set()方法 过程
到这里,ThreadLocal核心源码已经分析完毕了,估计你现在已经炸裂了吧,心想,这里什么牛人可以这么设计出来,牛人就是牛人,不得不佩服
既然大家看见这里,说明已经把前面的核心理念基本掌握,那么我在这里出几道ThreadLocal面试题,再让大家印象深刻点。。。
大家可以把答案写到评论中,欢迎讨论,答案下期给出,请期待
我是一个每天都在闲聊编程的迈莫
文章持续更新,可以微信搜索「 迈莫coding 」阅读,回复【888】有我准备的一线大厂面试资料