这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战
一、ThreadLocal的实现原理
Thread有一个内部变量ThreadLocal.ThreadLocalMap,这个类是ThreadLocal的静态内部类,它的实现与HashMap类似,当线程第一次调用ThreadLocal的get/set方法时会初始化它。它的键是这个ThreadLocal对象本身,值是需要存储的变量。也就是说ThreadLocal类型的本地变量是存放在具体的线程空间里。当不断的使用get方法获取时,是到线程独有线程空间中获取变量,使得其他线程无法访问到,也就达到了线程安全的目的。在使用完成之后,可以通过remove方法,移除不使用的本地变量。
ThreadLocal和同步机制的比较
如果说同步机制是一种以时间换空间的做法,那么ThreadLocal就是一种以空间换时间的做法,在同步机制下,当访问共享变量时,同步机制保证了同一个时刻只能有一个线程能访问到资源,其他线程会进入阻塞状态。而使用ThreadLocal,为每个线程都复制了共享变量的副本,也就不存在共享变量的说法。
二、源码
1.set方法
通过ThreadLocal的set方法调用到ThreadLocal.ThreadLocalMap静态内部类的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);
}
若内部ThreadLocalMap对象为空,则进入初始化threadLocalMap对象流程。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 先通过
hashcode作为下标取数组对应位置的值,若为空,设置值。若不为空,往后移动一个位置,如果获取到的长度等于数组长度,从0位置查找。 - 清除Entry对象还在,但是Entry的值为空的位置 && 当前数量是否大于容量扩容
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 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;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 2.
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这边的扩容有两个步骤:
- 重新排列table数组里的值,根据hashcode获取下标,若对应下标为空,则移动到该位置若下标位置不为空,往后移动位置,直到找到空位置。
- 排列的同时如果是空位置,会相应减少size,若排列之后的size仍然大于容量的3/4则扩容
private void rehash() {
//1.
expungeStaleEntries();
// 2.
if (size >= threshold - threshold / 4)
// 两倍原长度扩容
resize();
}
2.get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 默认值
return setInitialValue();
}
获取不到值有两种情况,e=null, e.get() == null,如果e=null直接返回null,如果e.get()=null,清除这个位置的值。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
三、内存泄漏
ThreadLcal的引用关系如上如所示,虚线是使用软引用的地方。现在假设这个地方使用的是强引用,在业务代码中使用threadlocalInstance==null将ThreadLocalRef和ThreadLocal之间的强引用置空,value还是会通过另一条引用链currentThread->currentThread->map->entry->value到达,也是不会被GC掉。而若采用软引用,在系统将要发生内存溢出时会回收掉,也就是会断掉key与ThreadLocal之间的引用,使得key=null。
在ThreadLocal的实现中,为了避免内存泄漏已经做了很多安全性的控制,在get()和set()方法中都有相应的处理,通过特定的方式对存在key=null的脏Entry进行value=null的处理,使得value的引用链不可达。
为什么使用弱引用?
一是尽管使用强引用也会出现内存泄漏,二是在ThreadLocal的生命周期中set、getEntry、remove里,都针对键为空的脏Entry进行处理。但是尽管如此,在编程过程中,形成一种良好的规范,在使用完ThreadLocal后都应该手动调用remove方法进行清理。