ThreadLocal中关于内存泄露的相关问题

974 阅读6分钟

简介

前言

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 酒吧;

测试工程师满意地离开了酒吧。

我看的肚子都饿了,就喊了句:给我来一份蛋炒饭!

结果,酒吧炸了!

最后,面对一份蛋炒饭的需求,我们可以说没有,但是不能炸。