看很多资料说Java8中ThreadLocal使用了虚引用以及set、get、remove会清理ThreadLocalMap中key为null的数据,这样就不会有内存泄露问题。真的是这样吗?如果是真的,key怎么为null的?怎么清理的?想找到答案,还是从源码入手。
一、set,直接定位到ThreadLocalMap.set
1):
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); -- 获取hash对应槽位
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) { -- 如果e不为null就获取下一个槽位,如果i=len-1则会再从0开始
ThreadLocal<?> k = e.get(); -- 获取当前槽位的key值
if (k == key) { -- 如果key值相同则直接替换
e.value = value;
return;
}
if (k == null) { -- key == null说明当前线程对ThreadLocal已无关联,但Entry还存在当前槽位中
replaceStaleEntry(key, value, i); -- 清空部分槽位后加入,下面讲
return; -- 满足条件直接返回
}
}
tab[i] = new Entry(key, value); -- 如果没有hash冲突或找到下一个空槽位则直接添加
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) -- 清空部分槽位并判断size是否到阀值
rehash(); -- 清空key为null的槽位,并增大数组长度
主要流程为:1、获取key的hash对应槽位
2、判断当前槽位是否已被占用,若已被占用,则判断key值是否相同,是则直接替换;否则则判断槽位中的
Entry的key值是否为null,为null则走replaceStaleEntry方法;
key不相同或不为null则一直往下找,直到遇到空槽位或key相同或key值为null的槽位
3、若直接找到为空的槽位,则放入并清空部分槽位并判断是否需要扩容
2):下面看replaceStaleEntry(key, value, i)逻辑,即添加数据时发生冲突并且冲突数据的key值为null
Entry[] tab = table;
int len = tab.length;
Entry e;
//清理数据的入口
int slotToExpunge = staleSlot; -- 传入的i,即key == null的槽位
for (int i = prevIndex(staleSlot, len); -- 从i位往前遍历数组,i=0时跳到len-1
(e = tab[i]) != null; -- 直到空槽位
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; -- 空槽位后第一个key为null的槽
for (int i = nextIndex(staleSlot, len); -- 从i往后遍历
(e = tab[i]) != null; -- 直到空槽位
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) { -- 如果找到key与传入的key相同,则替换, 并将新值替换到staleSlot位
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot) -- 如果staleSlot之前没有要清除的值,则从i位置开始
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); -- 清除数据
return;
}
if (k == null && slotToExpunge == staleSlot) -- 若果后面有需要清理的数据,但是前面没有,则设置清理点为i
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot) -- 如果还有需要清理的数据,则走清理逻辑
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
说明:1、找到第一个要清理的槽位位置slotToExpunge,如果有两个及以上的key为null的entry,则调用cleanSomeSlots
2、将要添加的值放到staleSlot位置
3):expungeStaleEntry逻辑,主要是value=null,释放value和entry,让垃圾收集器回收
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null; -- 释放value
tab[staleSlot] = null; -- 释放entry
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len); -- 从i往后遍历直到null
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; -- 释放value
tab[i] = null; -- 释放entry
size--;
} else { -- 若key不为null
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { -- 如果之前k出现过hash冲突,则将k放入冲突槽位后第一个为null的槽位
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i; --返回为staleSlot后第一个为空的槽位
}
说明:从staleSlot即清理点开始清除key为null的数据并返回下一个空槽位
1)、2)、3)代码可清除插入点前为空的槽位到后为空的槽位之间的key为null的数据,即两个空之间需要清除的entry已清除

4):cleanSomeSlots逻辑
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len); -- 从i往后遍历
Entry e = tab[i];
if (e != null && e.get() == null) { -- 遇到需要清除的key
n = len; -- 重置n
removed = true;
i = expungeStaleEntry(i); -- 清除i到第一个空槽位之间需要清除的key
}
} while ( (n >>>= 1) != 0); -- 每次循环n减半,即若2^x=len,则循环x+1次
return removed;
}
说明:从i位置遍历x+1个数据,若遇到需清理数据,则清除i到第一个空槽位之间需要清除的数据并重置n,否则退出。由于不是全遍历,所以还会有key=null的entry没有清除
总结:
set逻辑:1、若没有hash冲突,则直接插入,并调用cleanSomeSlots清除部分key为null的entry,若没有数据清除并且达到
扩容阀值,则进行扩容(扩容时会全遍历删除key=null的数据)
2、若有hash冲突:1):key相同则直接替换value;
2):若存在entry但是key为null,则将要添加的值插入, 并清理插入点前后两个空槽之间
key为null的数据,若两个空槽之间存在两个及以上个key为null的entry,则调用 cleanSomeSlots清除部分key为null的entry;
3):若没有key相同或entry.key为null的情况,则插入遍历到的第一个空槽,
并调用cleanSomeSlots清除部分key为null的entry,若没有数据清除并且达到
扩容阀值, 则进行扩容(扩容时会全遍历删除key=null的数据)
二、get,直接定位到ThreadLocalMap.getEntry,直接上源码
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e; -- 没有冲突时直接返回,没有清entry
else
return getEntryAfterMiss(key, i, e);
}
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); -- 同上,清理i两边空槽之间的key为null的值,
如果有两个及以上的key为null的entry,则调用cleanSomeSlots
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
说明:好像也不能全部清理掉
三、remove,直接定位到ThreadLocalMap.remove,直接上源码
private void remove(ThreadLocal<?> key) {
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(); -- 找到key并将entry.key设为null
expungeStaleEntry(i); -- 同上,清理i两边空槽之间的key为null的值,
如果有两个及以上的key为null的entry,则调用cleanSomeSlots
return;
}
}
}
说明:不能保证全部清除,但会清除当前key
总结:除了map扩容时会遍历整个数组进行清除外,其他方法都不能保证全部清除掉所有key为null的entry,除非线程本身被垃圾收集器回收,但现在用的最多的还是线程池,虽然大部分entry和value会被清理,但还会有部分一直存在内存中,所以也不能杜绝内存泄露,最好还是用完后手动remove为好。
注:目前版本key=null的entry是由于ThreadLocal在entry中是虚引用,在没有强引用时,会被垃圾垃圾收集器回收,回收掉后,entry中对应的key会为null。

2530





