前言
我朋友cute轩前几天面试,正好阿里爸爸看他读过JUC包下源码,直接提起面试官小哥哥的兴趣,直接死磕ThreadLocal源码,面完试已经汗流浃背了,犹如一场高手对决,辛亏他看完源码,才勉强应战。今天就好好分析ThreadLocal源码。
ThreadLocal是什么?
ThreadLocal是JUC包下提供的,它提供了本地变量,也就是让每个线程都有自己的独立空间来存储变量,且该变量不会受到其他线程的影响,也可以理解为每个线程都可以在自己的独立空间中操作变量,不会影响到其他线程中的变量值。
应用场景
当某些数据是以线程为作用域且不同的线程中变量副本不同,就考虑使用Threadlocal
ThreadLocal实现原理
ThreadLocal数据结构
如上类图可知Thread类中有一个threadLocals和inheritableThreadLocals都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认每个线程中这个两个变量都为null,只有当前线程第一次调用了ThreadLocal的set或者get方法时候才会进行创建。
ThreadLocalMap采用延迟加载策略,至少存放一个k-v值才会创建
ThreadLocalMap基本组成
private Entry
[] table
;
static class Entry extends WeakReference<ThreadLocal
<?>> {
Object value
;
Entry(ThreadLocal
<?> k
, Object v
) {
super(k
);
value
= v
;
}
}
ThreadLocalMap内部采用Entry对象封装键值对,并且将key值设置为弱引用,便于其回收 弱引用: 使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
get()分析
public T
get() {
Thread t
= Thread
.currentThread();
ThreadLocalMap map
= getMap(t
);
if (map
!= null
) {
ThreadLocalMap
.Entry e
= map
.getEntry(this);
if (e
!= null
) {
@SuppressWarnings("unchecked")
T result
= (T
) e
.value
;
return result
;
}
}
return setInitialValue();
}
get()方法流程:
首先会调用getMap()方法获取到当前线程对象ThreadLocal引用当ThreadLocal引用不为空时,则调用getEntry()方法获取到当前线程和当前ThreadLocal对象相关联的局部变量,若不为空,则返回若ThreadLocal引用为空或者调用getEntry()方法返回空值时,则需要调用setInitialValue()方法处理
setInitialValue()分析
private T
setInitialValue() {
T value
= initialValue();
Thread t
= Thread
.currentThread();
ThreadLocalMap map
= getMap(t
);
if (map
!= null
)
map
.set(this, value
);
else
createMap(t
, value
);
return value
;
}
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
)]) {
ThreadLocal
<?> k
= e
.get();
if (k
== key
) {
e
.value
= value
;
return;
}
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()分析
private void replaceStaleEntry(ThreadLocal
<?> key
, Object value
,
int staleSlot
) {
Entry
[] tab
= table
;
int len
= tab
.length
;
Entry e
;
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();
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
;
}
tab
[staleSlot
].value
= null
;
tab
[staleSlot
] = new Entry(key
, value
);
if (slotToExpunge
!= staleSlot
)
cleanSomeSlots(expungeStaleEntry(slotToExpunge
), len
);
}
ThreadLocalMap.replaceStaleEntry() 分析
从当前桶位的前一个桶位为起点,向前遍历直到遇到当前桶位为null,用slotToExpunge记录当前桶位从当前桶位的下一个桶位为起点,向后遍历,若在遍历过程中发现key值相等,则进行数据替换逻辑,如果slotToExpunge值不等于slot值的话,需要进行启发式清理,进行清理过期数据若条件二不成立的话,说明当前set操作是添加操作,在slot中添加数据,如果slotToExpunge值不等于slot值的话,需要进行启发式清理,进行清理过期数据
rehash()分析
private void rehash() {
expungeStaleEntries();
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
];
if (e
!= null
) {
ThreadLocal
<?> k
= e
.get();
if (k
== null
) {
e
.value
= null
;
} else {
int h
= k
.threadLocalHashCode
& (newLen
- 1);
while (newTab
[h
] != null
)
h
= nextIndex(h
, newLen
);
newTab
[h
] = e
;
count
++;
}
}
}
setThreshold(newLen
);
size
= count
;
table
= newTab
;
}
resize()方法 过程:
创建新的散列表,长度为旧散列表长度的两倍遍历旧散列表,若value值不为空时,将重新计算Hash值,将其填充到新的散列表中进行设置阈值 , 阈值默认为散列表长度的2/3
ThreadLocal#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
);
}
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】有我准备的一线大厂面试资料