ThreadLocal的一些说明:
ThreadLocal是一个泛型类,用于在线程中定义局部变量。Thread对象里面有一个默认修饰符修饰的ThreadLocalMap对象变量(ThreadLocalMap是ThreadLocal类里面一个默认修饰符修饰(本类和同包的类可以访问)的内部类)。
ThreadLocal本身并不存储值,它只是在线程的ThreadLocalMap变量中作为一个key来让线程从ThreadLocalMap获取value,每个value都是与ThreadLocal关联在一起的。ThreadLocal存储值,是通过Thread.currentThread()先获取到当前线程对象。由于Thread里面的ThreadLocalMap对象是默认修饰符修饰,而ThreadLocal与Thread位于相同的包中,所以在ThrealLocal类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过Thread对象可以访问到该线程的ThreadLocalMap对象。通过ThreadLocalMap的set方法,key为当前当前ThreadLocal对象,value为想设置的值,说得直白一点就是,ThreadLocal存储值,是通过线程自己内部的ThreadLocalMap存储的。而不同的线程自然就有不同的ThreadLocalMap对象变量。如果在同一个线程中声明了两个ThreadLocal对象,而在Thread内部都是使用仅有那个ThreadLocalMap存放数据的,ThreadLocalMap的key就是ThreadLocal对象,value就是ThreadLocal对象调用set方法设置的值。
通过上面的解释可以看出,ThreadLocal的get方法获取值,其实就是通过获取到当前线程的对象,然后通过当前线程获取到该线程的ThreadLocalMap对象,最后通过ThreadLocalMap拿到最开始设置的值。
ThreadLocal的set方法源码解析:
//ThreadLocalMap是ThreadLocal的一个默认访问修饰符修饰的内部类,也就是说ThreadLocalMap可以在ThreadLocal内部和同一个包的其他类中使用,
public void set(T value) {
//Thread内部定义了一个默认访问修饰符修饰的ThreadLocalMap变量,通过getMap方法可以获取到线程里的ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map不为空
map.set(this, value);
else
//如果map为空
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//由于Thread与ThreadLocal位于相同包,所以Thread里可以直接访问到线程里面默认访问修饰符修饰的ThreadLocalMap变量
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//将当前ThreadLocal对象作为key,新生成一个ThreadLocalMap对象,并将线程的threadLocals对象设置为这个新生成的ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
内部类ThreadLocalMap的一些说明:
ThreadLocalMap内部定义了弱引用的Entry用来存放key和value,定义了Entry数组存放Entry,数组的默认容量为16,并且数组的容量必须为2的幂,比如32,64,128等等。
1.内存泄露
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
2.为什么用弱引用?
key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。ThreadLocalMap在调用set、get、remove的时候会清除Entry数组中所有key为空的Entry,这样就避免了因为value没被回收而造成内存溢出。
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。 此段引用来自博客https://yq.aliyun.com/articles/574744
ThreadLocalMap声明的一些变量:
//ThreadLocalMap内部声明的Entry,用于存放key和value,key为ThreadLocal类型的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//内部Entry数组的默认初始容量为16,数组的容量必须为2的幂
//由于ThreadLocalMap内部有很多(容量-1)的与运算(&)计算数组索引,
//容量为2的幂可以保证(容量-1)转化为二进制后最后一位始终是1,这样可以有效的减少碰撞几率
private static final int INITIAL_CAPACITY = 16;
//内部Entry数组
private Entry[] table;
//数组中存放的元素数量
private int size = 0;
//数组扩容临界点
private int threshold;
ThreadLocalMap的set方法源码解析:
private void set(ThreadLocal<?> key, Object value) {
//table为ThreadLocalMap的Entry数组
Entry[] tab = table;
int len = tab.length;
//通过ThreadLocal类型的key计算索引位置i,采用线性搜索的方法搜索
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
//拿到Entry数组中位置i处的Entry的key
ThreadLocal<?> k = e.get();
//如果新增的key与k,相同则表示,当前数组存在相同的key的Entry了,此时只需要更新value
if (k == key) {
//更新value为新设置的值
e.value = value;
return;
}
//根据key计算的索引值,进行线性搜索后找到的第一个Key为空的Entry
if (k == null) {
//擦除key为空的Entry,并设置key和value
replaceStaleEntry(key, value, i);
return;
}
}
//如果方法没有在上面的方法中return,说明此时位置i的Entry是空的,可以设置key和value
tab[i] = new Entry(key, value);
int sz = ++size;
//cleanSomeSlots方法返回false表示数组中已经不存在key为空需要清除的Entry了,此时数组装满了,而 sz 表示此时数组中元素的数量大于临界值了时,需要调用rehash进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry方法说明:
该方法的结果就是,将key和value添加到Entry数组中,并且清除Entry数组中由于所有key为空的Entry。
replaceStaleEntry方法源码解析:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//staleSlot是在set方法中通过key计算索引,经过线性搜索,找到的第一个Key为null的Entry的所在位置,即:清除元素的开始位置
//从staleSlot位置反向搜索,因为Entry数组的设计是环形的,因此反向遍历可以遍历到最后一个Entry为null的位置
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i,len))
if (e.get() == null)
// 用slotToExpunge记录最后一个key为null的索引位置
slotToExpunge = i;
//从staleSolt向后搜索,直到遇见空的Entry为止
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i,len)) {
ThreadLocal<?> k = e.get();
//如果k与key相等,则将e的value设置为传入的value
if (k == key) {
e.value = value;
// 将i位置和staleSlot位置的元素对换,如此以来开始遍历的位置就是i位置了,减少了需要遍历的元素,提高遍历效率(staleSlot位置是要清除的元素)
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果slotToExpunge与staleSlot相等,则staleSlot位置表示的第一个key为null的Entry也是slotToExpunge表示的最后一个key为null的Entry,即表示数组中只有一个key为null的Entry,上面已经将staleSlot位置的Entry放到了i位置,则此时清除的开始位置slotToExpunge应该为i
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//从slotToExpunge位置开始清除key为空的Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果key为空,而根据slotToExpunge与staleSlot相等可以知道数组中只有一个key为空的Entry,所以此时开始清除的位置slotToExpunge就是当前遍历到的位置i
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 上面的遍历没有遇见空的Entry,则将staleSlot位置的value设为空,并且在此位置放入新的Entry对象
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果slotToExpunge不等于staleSlot表示,第一个key为空的Entry和最后一个key为空的Entry 不是同一个,也就是说Entry数组中存在多个Entry中key为空的对象,则从slotToExpunge位置开始清除
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
expungeStaleEntry方法说明:
方法expungeStaleEntry执行的结果是,从 staleSlot位置开始到最后i位置之间的所有key为空的Entry都被清空。最后返回的位置i 是从staleSlot位置开始向后线性搜索到的第一个Entry为空的位置。
源码解析:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//将Entry数组中staleSlot位置的元素设为空,并且该位置Entry的value也设置为空,总数size减一
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
//下面就是将数组重新
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
//从staleSlot位置处向后遍历每个位置的Entry,只要Entry里key为空,则删除该位置的元素,将该位置的Entry以及Entry里的value设置为null
ThreadLocal<?> k = e.get();
if (k == null) {//Entry里的key为空,则删除此位置的数据
e.value = null;
tab[i] = null;
size--;
} else {
//如果Entry里的key不为空,则再次计算索引位置,如果通过key计算的索引位置h与当前索引位置i不相同,则需要将Entry放在数组的新位置中
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
//将此时i位置设为空
tab[i] = null;
//线性搜索数组中空的位置,将e放入到该位置,
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//执行到最后可以得出,从staleSlot位置到i位置之间所有key为空的Entry都被清除了
return i;
}
cleanSomeSlots方法源码解析:
该方法可以清除Entry数组中所有key为空的Entry
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//从i位置向后遍历
i = nextIndex(i, len);
Entry e = tab[i];
//找到一次key为空的Entry,就执行一次连续清理
if (e != null && e.get() == null) {
n = len;
removed = true;
//执行一次连续段清理
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}