简介
前言
ThreadLocal 相信大家都不陌生,虽然在实际场景中我们很少使用,但因为它在 Handler 机制中发挥的重要作用,成了我们必须要去探究的对象,也是技术面试中的高频考点。
概念
直观的说,ThreadLocal 提供了 set 和 get 方法,用于将对象与当前的线程绑定,就像 Handler 机制中的 Looper 一样,通过如下两行代码可以分别在当前线程中新建和获取 Looper 对象,并且该对象和其他线程完全隔离。
Looper.prepare();
Looper looper = Looper.myLooper();
原理
关于 ThreadLocal 的实现原理并不复杂,相关的技术文章也已经很多了。这里我用一张图片简单概括一下。
如上图,每个 Thread 中都维护着一个 ThreadLocalMap 对象,我们把要绑定的目标对象作为 Value,当前的 ThreadLocal 对象作为 Key,并将该键值对添加到当前线程的 ThreadLocalMap 对象中。
每次调用 ThreadLocal 的 get 或者 set 方法时,首先会获取当前线程的 ThreadLocalMap 对象,再用当前 ThreadLocal 对象作为 Key 进行存取。而且这里我们注意到,图中作为 Key 的 ThreadLocal 对象是用虚线标明的,这是因为在源码中,该引用使用了弱引用。这也是接下来讨论的重点,ThreadLocal 中对内存泄露的处理。
ThreadLocal 中内存泄露的处理
弱引用
我们知道,内存泄露的本质,是一个本该被释放的对象,却仍然被 GC root 直接或间接通过强引用连接着,导致其无法清除,从而导致了内存泄露。在某些场景下,解决方式就是把强引用改为弱引用,只被弱引用连接着的对象,在 GC 发生时,会被清除。(这里区别于软引用,软引用是只在内存不足的时候才会释放,而弱引用不管内存如何,都会释放)
ThreadLocal 中弱引用的用法
继续回到 ThreadLocal 中来,ThreadLocalMap 中的 Key 通过弱引用的方式指向 ThreadLocal 对象,所以当 ThreadLocal 对象不再使用时,不会因为被 ThreadLocalMap 引用而造成内存泄露。
那 Key 的引用解决了,那 Value 怎么说呢。我们查看 ThreadLocalMap 类中,get,set,remove 等相关方法,会发现最后都会调用一个 expungeStaleEntry(int staleSlot) 方法。
该方法会遍历 map 中的元素,检查 Entity 不为空但是 Key 为空的元素,并清除。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
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 {
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;
}
通过 remove 方法防止内存泄漏
我们注意到 ThreadLocal 中提供了 remove 方法,并且最终会调用 ThreadLocalMap 中的 remove 来清除相关的 Entry,那我们直接在 ThreadLocal 使用结束前调用 remove 方法,就可以直接移除相关 Entry,也就不用执行前面那么复杂的逻辑了。没错,这也是规范的 ThreadLocal 的使用方式,并且不会导致内存泄露,正如我们在 Handler 使用结束前需要执行 removeCallbacksAndMessages 方法一样。
那,之前那套逻辑存在的意义是什么呢?又是弱引用,又要清理 Key 为空的元素。
其实,这个才是我最想说的。
我们阅读源码的意义,并不只是为了查看其实现方式,更不是为了面试需要。而是为了学习优秀代码的编写思路和设计思想,有哪些点可以被我们借鉴过来,提高我们自己的代码水平。
继续说回到 ThreadLocal,这一套机制是设计给上层开发人员调用的,规范的用法是在使用结束前,需要调用 remove 方法来移除元素,避免内存泄漏。但是,设计人员必须要考虑到,并不是所有的人都会按照规范使用,一定会有人忘记调用 remove,这个时候,怎么才能把损失降到最低。所以才有了弱引用这套方案,这样能保证在忘记调用 remove 方法的情况下,保证 Key 会在下一次 GC 中被清除,Value 也会在其他 ThreadLocal 对象调用 set,get 等方法的过程中被清除。
好的程序一定要保证其健壮性,我们不能假定用户一定会按照我们预想的路径去使用,哪怕面对的是专业的开发人员,设计者也需要考虑到在不规范使用的情况下,如何能将损失降到最低。我们自己在面向用户编写程序的时候也要注意,不能只按照正常的使用路径去思考,一定要考虑到各种极端情况和边界情况,这样的程序才是健壮的。
这是我在 ThreadLocal 的学习中,最大的收获。
最后,再引用一个知名段子
一个测试工程师走进酒吧,要了-1杯啤酒;
一个测试工程师走进酒吧,要了2^32杯啤酒;
一个测试工程师走进酒吧,要了一杯洗脚水;
一个测试工程师走进酒吧,要了一杯蜥蜴;
一个测试工程师走进酒吧,要了一份asdfQwer@24dg!&*(@;
一个测试工程师走进酒吧,什么也没要;
一个测试工程师走进酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来;
一个测试工程师走进酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿;一个测试工程师走进酒吧,要了一杯烫烫烫的锟斤拷;
一个测试工程师走进酒吧,要了NaN杯Null;
一个测试工程师冲进酒吧,要了500T啤酒咖啡洗脚水野猫狼牙棒奶茶;
一个测试工程师把酒吧拆了;
一个测试工程师化装成老板走进酒吧,要了500杯啤酒并且不付钱;
一万个测试工程师在酒吧门外呼啸而过;
一个测试工程师走进酒吧,要了一杯啤酒';DROP TABLE 酒吧;
测试工程师满意地离开了酒吧。
我看的肚子都饿了,就喊了句:给我来一份蛋炒饭!
结果,酒吧炸了!
最后,面对一份蛋炒饭的需求,我们可以说没有,但是不能炸。