gt;= 1) != 0);
return removed;
}
参数 n
表示扫描控制。初始情况下扫描 log2(n)
次,如果遇到过期条目,会再扫描 log2(table.length)-1
次。在 set()
方法中调用,参数 n
表示元素的个数。在 replaceStaleEntry
中调用,参数 n
表示的是数组 table
的长度。
注意 do 循环里面的判断条件:e != null && e.get() == null
,还是那些 Entry 不为空,key 为空的过期条目。发现过期条目之后,调用 expungeStaleEntry()
去清理。
expungeStaleEntry()
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 清空 staleSlot 处的 过期 entry
// 将 value 置空,保证不会因为这里的强引用造成 memory leak
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
// 继续搜索直到遇到 tab 中的空 entry
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { // 搜索过程中遇到过期条目,直接清理
e.value = null;
tab[i] = null;
size--;
} else {
// key 还没有被回收
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i; // 此时从 staleSlot 到 i 之间不存在过期条目
}
直接将 entry.value
和 entry
都置空,消除内存泄露的隐患。注意这里仅仅只是置空,并不是回收对象。因为你不知道 value
在外部的引用情况,只需要管好自己的引用就可以了。
除此之外,不甘寂寞的 expungeStaleEntry()
又发起了一次扫描,直到碰到空 Entry未知。期间遇到的过期 Entry 要置空。
整个 set()
方法就看完了,原理很简单,但是其中关于内存泄漏的预防处理十分复杂,看的我一度放弃了,也让我对源码阅读产生了一些疑问。有些时候是不是没有必要逐行去玩去完全理解?比如这一系列关于内存泄露的处理,核心思想就是清理 Entry 不为 null 但 key 为 null 的过期条目。理解了核心思想,对于其中复杂的细节处理是不是没有必要去深究?不知道你怎么看,欢迎在评论区写下你的看法。
下面来看一看 getgetEntry
方法。
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;
else
// 未直接命中,线性探测,继续往后找
return getEntryAfterMiss(key, i, e);
}
getEntry()
比较粗暴,上来直接根据哈希值查找 table 数组,如果直接命中,就返回。未直接命中,调用 getEntryAfterMiss()
继续查找。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 向后查找直到遇到空 entry
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key) // get it
return e;
if (k == null) // key 等于 null,清理过期 entry
expungeStaleEntry(i);
else
i = nextIndex(i, len); // 继续向后查找
e = tab[i];
}
return null;
}
调用 nextIndex()
向后查找,直到遇到 空 Entry,也就是队尾:
k==key
,说明找到了对应 Entry
k==null
,说明遇到了过期 Entry,调用 expungeStaleEntry()
处理
对过期 Entry 的处理真的是无处不在,就是为了最大程度的降低内存泄漏发生的几率。那么有没有什么一劳永逸的办法呢?那就是 ThreadLocalMap
的 remove()
方法。
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();
expungeStaleEntry(i);
return;
}
}
}
直接清除当前 ThreadLocal 对应的 Entry,根本上避免了发生内存泄露。所以,当我们不再需要使用 ThreadLocal 中的相应数据时,调用一下 remove()
方法肯定是个好习惯。
虽然在长期存活的线程(例如线程池)中使用 ThreadLocal
并发生内存泄漏是一个小概率事件,但 JDK 开发者却为此多写了很多代码。我们在使用中也要多加注意,仔细考虑是否会涉及到内存泄露的问题。
End
最后说说在网上看到的一个观点,ThreadLocal 比 Synchronized 更适合解决线程同步问题。
首先这个问题本身就不是那么严谨。ThreadLocal
是用来解决线程同步问题的吗?表面上看,ThreadLocal
的机制的确是线程安全的,但它并不是为了解决多线程访问同一个变量的竞争问题,而是给每一个线程都提供单独的变量,有些文章称之为 数据备份,但它们并不是备份,每一个都是独立存在的,互不干扰,并不存在什么同步问题。
ThreadLocal
和 Synchronized
的应用场景也是千差万别的。例如银行的转账场景,涉及多个账户同时转账的多线程同步问题,ThreadLocal
根本就没法解决,即使每个线程都单独保存着用户的余额也没法解决并发问题。ThreadLocal
在 Android 中的典型应用就是 Looper
,每个线程都有自己的 Looper
对