前言
我朋友cute轩前几天面试,正好阿里爸爸看他读过JUC包下源码,直接提起面试官小哥哥的兴趣,直接死磕ThreadLocal源码,面完试已经汗流浃背了,犹如一场高手对决,辛亏他看完源码,才勉强应战。今天就好好分析ThreadLocal源码。
ThreadLocal是什么?
ThreadLocal是JUC包下提供的,它提供了本地变量,也就是让每个线程都有自己的独立空间来存储变量,且该变量不会受到其他线程的影响,也可以理解为每个线程都可以在自己的独立空间中操作变量,不会影响到其他线程中的变量值。
应用场景
- 当某些数据是以线程为作用域且不同的线程中变量副本不同,就考虑使用Threadlocal
ThreadLocal实现原理
ThreadLocal数据结构
如上类图可知Thread类中有一个threadLocals和inheritableThreadLocals都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认每个线程中这个两个变量都为null,只有当前线程第一次调用了ThreadLocal的set或者get方法时候才会进行创建。
ThreadLocalMap采用延迟加载策略,至少存放一个k-v值才会创建
ThreadLocalMap基本组成
// 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修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
get()分析
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()方法流程:
- 首先会调用getMap()方法获取到当前线程对象ThreadLocal引用
- 当ThreadLocal引用不为空时,则调用getEntry()方法获取到当前线程和当前ThreadLocal对象相关联的局部变量,若不为空,则返回
- 若ThreadLocal引用为空或者调用getEntry()方法返回空值时,则需要调用setInitialValue()方法处理
setInitialValue()分析
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流程:
- 首先会调用getMap()方法获取到当前线程对象ThreadLocal引用
- 当ThreadLocal引用不为空时,则调用ThreadLocalMap类中set()进行数据覆盖
- 若ThreadLocal引用为空的话,则会调用createMap()方法进行创建ThreadLocal对象
ThreadLocalMap.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)]) {
// 获取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,然后从该桶位进行遍历,若在遍历过程中,遇到key值相等的情况下,则说明此次过程是替换过程;若key为空,表示该key值已被GC回收,则需要执行探测式替换逻辑;否则走步骤二
- 走到这步,说明已经找到限制的桶位,将k-v封装成Entry对象填充到该桶位中
- 最后需要检测是否需要扩容
ThreadLocalMap.replaceStaleEntry()分析
// 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() 分析
- 从当前桶位的前一个桶位为起点,向前遍历直到遇到当前桶位为null,用slotToExpunge记录当前桶位
- 从当前桶位的下一个桶位为起点,向后遍历,若在遍历过程中发现key值相等,则进行数据替换逻辑,如果slotToExpunge值不等于slot值的话,需要进行启发式清理,进行清理过期数据
- 若条件二不成立的话,说明当前set操作是添加操作,在slot中添加数据,如果slotToExpunge值不等于slot值的话,需要进行启发式清理,进行清理过期数据
rehash()分析
private void rehash() {
// 清理掉当前散列表内的所有过期的数据
expungeStaleEntries();
// 当前散列表长度是否大于阈值*0.75
if (size >= threshold - threshold / 4)
// 扩容操作
resize();
}
rehash()方法 过程
- 首先清除当前散列表中所有过期数据
- 判断当前散列表长度是否大于阈值的0.75倍,若大于,才进行扩容处理
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];
// 条件成立 说明当前桶位值未被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()方法 过程:
- 创建新的散列表,长度为旧散列表长度的两倍
- 遍历旧散列表,若value值不为空时,将重新计算Hash值,将其填充到新的散列表中
- 进行设置阈值 , 阈值默认为散列表长度的2/3
ThreadLocal#set()分析
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()方法 过程
- 调用getMapp方法获取当前线程对应的ThreadLocalMap对象,若ThreadLocalMap对象为空,则调用ThreadLocalMap#set方法进行添加k-v键值对,该方法上面有具体流程;否则的话,执行步骤二
- 调用createMap方法进行创建ThreadLocalMap对象
到这里,ThreadLocal核心源码已经分析完毕了,估计你现在已经炸裂了吧,心想,这里什么牛人可以这么设计出来,牛人就是牛人,不得不佩服
既然大家看见这里,说明已经把前面的核心理念基本掌握,那么我在这里出几道ThreadLocal面试题,再让大家印象深刻点。。。
灵魂拷问
- ThreadLocalMap的Hash算法?
- ThreadLocalMap中Hash冲突如何解决?
- ThreadLocalMap扩容机制?
- ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程?
- ThreadLocalMap.set()方法实现原理?
- ThreadLocalMap.get()方法实现原理?
大家可以把答案写到评论中,欢迎讨论,答案下期给出,请期待
总结
我是一个每天都在闲聊编程的迈莫
文章持续更新,可以微信搜索「 迈莫coding 」阅读,回复【888】有我准备的一线大厂面试资料