Android 的消息机制 Looper 的实现就采用了 ThreadLocal ,使用 ThreadLocal 可以实现数据隔离,那它是怎么实现的呢?现在来分析一波,先看它的 set() 流程。
一、set(T value) 流程分析
在这里,通过调用 ThreadLocal 的 set 方法,这里很简单,只有简单的几个步骤:
- 获取调用这个方法的线程。
- 获取 ThreadLocalMap,并且对 map 进行判空处理。
其实 ThreadLocalMap 就类似与 HashMap ,使用的是 key-value 的映射,这里可以暂先这样理解,后面会慢慢分析,接下来看 get 的流程。
二、get() 流程分析
这里话同样是先获取到了当前的线程和 ThreadLocalMap ,然后对 map 进行判断处理。先看 map 为空的处理:
感觉是不是和 set 流程似曾相识。在这里,如果 get 没有获取到元素或者 map 为空的话,就会走进这个流程里面。initialValue() 实际上返回了 null,也就是说,如果 get 没有获取到元素或者 map 为空的话,会创建 map 并且返回 null。
接下来看 map 不为空的流程:
这里的话很好理解,先通过 key 的hashcode 和 数组长度 – 1 进行 & 运算获取 value 对应的索引值,然后通过索引值获取value 。
三、ThreadLocalMap
之前说过 ThreadLocalMap 是一个基于 key-value 的映射,那么这里的 key 和 value 分别是什么呢?通过之前的我分析可以看到,value 的ThreadLocal 所引用的泛型类,而这里的 key,其实就是这个 ThreadLocal 变量。
每一个线程都有一个 ThreadLocalMap,key 对应着这个 ThreadLocal 变量,value 对应着这个 T 类型的对象,接下来继续对set 流程进行分析。
之前对 ThreadLocal 的 set 方法进行了简单的分析,它会对 map 进行判空处理,如果获取的 map 为空,最终会走到下面的这个方法里面来:
在这里,可以分成以下几个步骤:
- 创建一个 table 数组。
- 获取索引值,获取方式之前讲过了。
- 在这个索引所在的位置,创建一个节点。
- 设置阈值,注意这里不是数组长度的 0.75,而是 三分之二。
接下来看 map 不为空的流程:
在这个可以看到,通过这个 key 拿到了索引之后,就会顺着这个索引的位置往下寻找,直到找到一个索引位置不存在节点的位置,然后在这个位置,使用 value 创建一个新的节点,之后才会判断是否需要扩容。
可以看到,这里的 set 的非常简单,如果发生了哈希冲突,就采用线性探测的方法,而不是像 HashMap 那样采用了链表 + 红黑树的数据结构。
四、内存泄漏的分析
ThreadLocal 为什么会发生内存泄漏呢?我们来看看 ThreadLocalMap 中 Entry 节点的相关代码:
可以看到这个 Entry 节点继承自 WeakReference,构造方法中对 key 进行了弱引用的处理,但是value依然是强引用,现在问题就来了。如果这个 key 没有被外部对象强引用的时候,就会在发生 GC 的时候被回收掉,但是它对应的 value 不会被回收,如果创建 ThreadLocal 的线程一直运行下去 Entry 对象对应的 value 就会一直得不到回收,从而发生内存泄漏。
解决的方法很简单,我们需要手动调用 remove 方法。