1.Entry?弱引用?
上一讲我们简单看了ThreadLocal的入门,了解到ThreadLocal是如何保证填充的变量是属于当前线程的。这里我们继续分析ThreadLocal源码。上一讲我们最后看到了ThreadLocalMap类的定义,发现其中定义了一个内部类Entry继承了WeakReference,代码如下:
/**
* Entry类继承WeakReference,这是一个弱引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
/**
* 这里对ThreadLocal k的引用是若引用 若ThreadLocal没有再被其他有效数据强引用着,则下次垃圾回收的时候会将k对象回收掉
*/
Entry(ThreadLocal<?> k, Object v) {
super(k); //对key是弱引用 即对ThreadLocal是若引用
value = v; //对value是强引用
}
}
观察其构造方法,接收了两个参数,一个是我们的ThreadLocal对象,另一个是我们要往ThreadLocal中填充的变量Object,Entry类定义了一个Object value的属性,Entry对value的引用是强引用,而对于另一个参数ThreadLocal对象的处理交给了父类WeakReference,这代表Entry对ThreadLocal<?> k的引用是一个弱引用,只被弱引用关联的对象将会在下一次垃圾回收时被回收掉。
2.弱引用+线程池 导致的内存泄漏问题
ThreadLocal对象结合多线程使用,在没有使用线程池的情况下,线程执行完毕会被销毁,那么包含Thread中的ThreadLocalMap,以及ThreadLocalMap中的Entry数组table,以及数组中的Entry对象,包含Entry对象中的value都属于不可达对象,都将会在下一次垃圾回收的时候被回收掉,也就不存在内存泄漏的问题。
可是,当我们使用线程池的时候,核心线程执行完当前任务后并不会被销毁,而是会去执行下一个任务,也就是说ThreadLocalMap会一直被Thread引用着,不会被GC包括ThreadLocalMap中的Entry数组table,以及数组中的Entry对象,包含Entry对象中的value,可是由于Entry对象对ThreadLocal的引用是弱引用,当任务执行完毕,任务会被销毁,对应的ThreadLocal对象也会被清理掉。此时,ThreadLocalMap中的value对应的key就会变成null,我们就无法获取到这个value,这样就会造成内存泄漏的问题。
3.如何避免内存泄漏问题
方案1:提供方法,在ThreadLocal使用完成之后调用方法清除ThreadLocalMap中的value,感觉可行,但不保证别人使用之后一点给会调该方法。
方案2:定时对Thread中的ThreadLocalMap中key为null的value值进行清理,需要搞定时相关东西,麻烦,复杂,Pass掉该方案
方案3:还是要对Thread中的ThreadLocalMap中key为null的value值进行清理,改成触发机制,在进行某些操作的时候触发该机制。 感觉可行,在特殊情况下,某个任务之后若没有再使用到ThreadLocal,那么对于之上还未处理的value,还是存在内存泄漏问题。
总结:相对来说,方案2最为保险,但是实现起来比较复杂,而且会占用更多的资源。方案1和方案3在特殊情况下都存在一定的问题,若是13两个方案一起使用,出现问题的几率就大大的降低了。
豆豆:别给这瞎猜了,去看源码吧。
。。。。。。
一顿秃头式的啃源码之后,发现确实跟我想的一样,是用的13两个方案一起。下面我们一起来看一下
3.1 ThreadLocal中的remove
/**
* 移除数据
*/
public void remove() {
//获取当前线程,并获取当前线程中的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) //ThreadLocalMap不为null的时候
m.remove(this); //调用其remove方法将this(ThreadLocal移除调)
}
这里调用了ThreadLocalMap的remove方法,我们看一下这个方法
/**
* 移除
*/
private void remove(ThreadLocal<?> key) {
//获取Entry数组
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)]) {
if (e.get() == key) {
e.clear(); //清除Entry对ThreadLocal key的弱引用
expungeStaleEntry(i); //调用expungeStaleEntry进行处理
return; //返回
}
}
}
这里会调用expungeStaleEntry方法进行处理,直接翻译意思是清除无用的Entry,我们看一下其对应的源码
/**
* 清除无用的Entry
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table; //获取Entry数组
int len = tab.length; //获取数组长度
tab[staleSlot].value = null; //将数组中staleSlot位置数据的value置空(取消强引用)
tab[staleSlot] = null; //将数组的staleSlot位置的Entry对象置空
size--; //存储数据数-1
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null; //遍历条件tab[i] != null
i = nextIndex(i, len)) { //从当前位置的下一个节点开始向后遍历
ThreadLocal<?> k = e.get(); //获取Entry对象对应的k 也就是ThreadLocal对象
if (k == null) { //判断对应的k为null 说明对应的ThreadLocal对象已被GC
e.value = null; //将对应value置空
tab[i] = null; //将数组中对应位置置空
size--; //存储数据-1
} else { //k 不为空的情况处理 即k还未被GC
int h = k.threadLocalHashCode & (len - 1); //根据k 计算下标
if (h != i) { //下标不为i 即无法通过该k获取到i下标的数据
tab[i] = null; //直接清除数组中对应下标位置
while (tab[h] != null)//从数组中 k对应下标h位置开始遍历,找到第一个Entry为空的位置 h
h = nextIndex(h, len);
tab[h] = e; //将k对应节点信息直接放到数组中下标为h的位置
}
}
}
return i; //返回结果
}
这里发现会清理掉没用的数据,并调整后面数据的位置,以上就是ThreadLocal中remove方法处理的全过程。
3.2 ThreadLocal中的get方法
这里的get方法也会处理ThreadLocalMap中的无效数据的,我们看一下它是如何进行的
/**
* 获取数据
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取对应ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) { //map不为null
//根据当前ThreadLocal对象获取map中对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) { //若节点e存在
@SuppressWarnings("unchecked")
T result = (T)e.value; //获取节点中对应的value
return result; //返回value
}
}
//map为null或者map中未获取到对应的Entry 调用setInitialValue方法
return setInitialValue();
}
我们先分析setInitialValue方法,这个方法会在map == null或者map中未获取到对应的Entry时调用,这里会返回一个默认的值
/**
* 设置初始值并返回值
*/
private T setInitialValue() {
//调用initialValue设置初始值,这里直接返回null,子类可覆盖重写该方法
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取线程对应ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); //若map不为null 将值set进map中
else
createMap(t, value); //初始化map并指定值
return value; //返回创建的初始值
}
就是这个样子,然后我们分析一下map不为null的时候,这是会调用ThreadLocalMap的get方法获取值,传入了当前线程。
/**
* 根据key查找数据 返回对应Entry
*/
private Entry getEntry(ThreadLocal<?> key) {
//计算下标
int i = key.threadLocalHashCode & (table.length - 1);
//获取数组中对应下标的数据
Entry e = table[i];
//判断节点是否非空并且是当前节点
if (e != null && e.get() == key)
return e;
else //e为空或e不是当前节点 会调用getEntryAfterMiss方法进行处理
return getEntryAfterMiss(key, i, e);
}
如上注解所示。在看一下getEntryAfterMiss方法的处理
/**
* 未直接查询到数据的操作
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table; //获取数组
int len = tab.length; //获取数组长度
while (e != null) { //遍历节点,直到e == null
ThreadLocal<?> k = e.get(); //获取节点对应的ThreadLocal k
if (k == key) //判断是否为当前ThreadLocal对象key
return e; //若是当前key 返回当前节点
if (k == null) //若k为null
expungeStaleEntry(i); //从i开始清除无用的节点
else
i = nextIndex(i, len); //获取下一个节点下标
e = tab[i]; //获取下标对应数据
}
//走到这里说明未找到对应节点的数据
return null; //返回null
}
又看到了这个expungeStaleEntry方法,到这里我们看出ThreadLocal在调用get方法的时候也会对Thread中的ThreadLocalMap对象上的数组进行一次清理。
3.3 ThreadLocal中的set方法
这里的set方法也会处理ThreadLocalMap中的无效数据的,我们也来看一下它是如何进行的
/**
* 设置值
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取Thread类中维护的ThreadLocalMap 默认是null
ThreadLocalMap map = getMap(t);
//若map不为null 调用其set方法
if (map != null)
map.set(this, value);
else
//map 为null 调用createMap方法
createMap(t, value);
}
第一次set时createMap这里不用说,肯定时没有进行清理的,这时候肯定也是不需要清理的,我们看一下map.set方法
/**
* 设置值的set方法
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table; //获取Entry数组
int len = tab.length; //获取当前数组大小
int i = key.threadLocalHashCode & (len-1);//计算下标
for (Entry e = tab[i]; //获取e
e != null; //e不为null
e = tab[i = nextIndex(i, len)]) { //获取下一个位置 0到len-1循环
ThreadLocal<?> k = e.get(); //获取k
if (k == key) { //若key相等 覆盖value值
e.value = value;
return; //返回
}
if (k == null) { //若节点为空
replaceStaleEntry(key, value, i); //调用replaceStaleEntry方法处理
return; //直接返回
}
}
//走到这里说明 (e = tab[i]) == null
tab[i] = new Entry(key, value); //直接将数据放入
int sz = ++size; //计算新的大小并赋值给sz
if (!cleanSomeSlots(i, sz) && sz >= threshold) //没有节点可清理并且sz达到扩容阈值
rehash(); //扩容操作
}
这里计算key对应到Entry数组中的下标位置,然后从当前节点进行检查,结果分以下情况:
1,若查找到key相同的节点,则覆盖value
2,若找到节点非空,但其key为空,则替换该节点信息
3,若找到一个null节点,就新建节点放入这个位置
最后还要检查当前是否需要扩容,若需要,则进行扩容操作
下面我们先看一下replaceStaleEntry方法的内容
/**
* 替换节点
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table; //获取数组
int len = tab.length; //获取数组长度
Entry e;
int slotToExpunge = staleSlot; //当前下标
/**
* 从staleSlot前一个位置向前遍历数据直到出现一个null节点
* 最后slotToExpunge记录staleSlot以及位置之前这一批非null节点中的第一个key为null的节点
*/
for (int i = prevIndex(staleSlot, len); //获取前一个下标
(e = tab[i]) != null;//循环条件tab[i] != null
i = prevIndex(i, len))//向前遍历
if (e.get() == null) //若节点的弱引用值为null
slotToExpunge = i; //记录当前下标
/**
* 从staleSlot下一个位置向后遍历数据直到出现一个null节点
*/
for (int i = nextIndex(staleSlot, len); //从当前下标向后
(e = tab[i]) != null; //循环条件tab[i] != null
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();//获取k
if (k == key) { //若key == key
e.value = value; //覆盖值
tab[i] = tab[staleSlot]; //将staleSlot位置的数据赋值到i位置
tab[staleSlot] = e; //将e节点放到staleSlot位置上
//slotToExpunge == staleSlot说明,staleSlot之前未出现节点非空但key为null的数据
if (slotToExpunge == staleSlot)
slotToExpunge = i; //记录slotToExpunge的值为i
//清除数据
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return; //返回结果
}
//slotToExpunge == staleSlot说明,staleSlot之前未出现节点非空但key为null的数据
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//情况之前的几点数据
tab[staleSlot].value = null;
//在staleSlot位置放入新的节点
tab[staleSlot] = new Entry(key, value);
//slotToExpunge != staleSlot说明,staleSlot之前出现了节点非空但key为null的数据
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清除数据
}
然后看一下cleanSomeSlots方法的处理
/**
* 清除一些过期数据
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false; //设置removed = false
Entry[] tab = table; //获取Entry数组
int len = tab.length; //获取数组长度
//循环
do {
i = nextIndex(i, len); //获取下一个节点位置
Entry e = tab[i]; //获取节点数据
if (e != null && e.get() == null) { //若数据存在但已无效
n = len; //设置n为数组长度
removed = true; //设置removed为ture
i = expungeStaleEntry(i); //调用expungeStaleEntry方法清除无用的Entry
}
} while ( (n >>>= 1) != 0); //n 无符号右移 1位不为0
return removed;
}
最后就是扩容方法
/**
* 扩容操作
*/
private void rehash() {
//先调用expungeStaleEntries方法清理数控
expungeStaleEntries();
//清理数据之后还存在 size >= threshold * 3 / 4
if (size >= threshold - threshold / 4)
resize(); //调用resize方法进行扩容
}
/**
* 清理数据
*/
private void expungeStaleEntries() {
Entry[] tab = table; //获取数组
int len = tab.length; //获取大小
for (int j = 0; j < len; j++) { //遍历数组
Entry e = tab[j]; //获取节点信息
if (e != null && e.get() == null) //若节点存在但无效
expungeStaleEntry(j); //调用expungeStaleEntry方法清理数据
}
}
/**
* 扩容操作
*/
private void resize() {
Entry[] oldTab = table; //获取数组
int oldLen = oldTab.length; //获取大小
int newLen = oldLen * 2; //获取新大小 是原大小的2倍
Entry[] newTab = new Entry[newLen]; //创建新的数组
int count = 0; //设置count为0
//遍历数组
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];//获取节点信息
if (e != null) { //节点非空
ThreadLocal<?> k = e.get(); //获取对应key
if (k == null) { //key为空
e.value = null; //清除对应value
} else { //key非空的情况
int h = k.threadLocalHashCode & (newLen - 1);//计算数据在新数组中的位置
while (newTab[h] != null) //从当前位置开始向后查找到第一个空节点位置h
h = nextIndex(h, newLen);
newTab[h] = e; //将节点放入到h位置
count++; //计算器+1
}
}
}
//设置新的扩容阀值
setThreshold(newLen);
size = count; //将count赋值给size
table = newTab; //将新数组赋值给table
}
关于ThreadLocal就先到这里吧