看很多资料说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。