源码阅读笔记 之 ThreadLocal —— 不复杂,却有点绕的一个 per thread API
(已于20201014重新更新)
ThreadLocal 源码阅读笔记:
1. 初探 ThreadLocal:
从类层面上看,ThreadLocal 中定义了 ThreadLocalMap。操作这个map需要通过 ThreadLocal 提供的 public API,常用的API有 get(), set(T) 等。
从对象层面上看,Thread 类里面封装了一个类型为 ThreadLocal.ThreadLocalMap、名称为 threadLocals 的实例属性。
从代码运行层面上看,操作的类型顺序为 ThreadLocal --> Thread --> ThreadLocalMap --> ThreadLocal --> <和ThreadLocal对象绑定的 Object>。第一次访问 ThreadLocal 是因为操作 ThreadLocalMap 的 API 封装在 ThreadLocal 中,而这个map是Thread对象的某个属性,需要通过Thread对象访问;访问到map后,仍需要ThreadLocal作为map的key来查找当前Thread所使用的某个ThreadLocal对象绑定的 Object。
你没看错,是“某个ThreadLocal对象”。这意味着,Thread可以拥有多个ThreadLocal,即在代码中让Thread操作多个ThreadLocal对象(或者说拥有它们的访问权),这些对象在 ThreadLocalMap 中遵从哈希表存放 key-val 对的基本规则。下面讲 ThreadLocalMap 中如何解决哈希碰撞。
2. ThreadLocal 中 ThreadLocalMap 的 hash碰撞解决策略:线性探测法。
前面说到“ThreadLocalMap 中遵从哈希表存放 key-val 对的基本规则”,因此当你的 Thread 使用了多于一个 ThreadLocal 对象时,就会在 ThreadLocalMap 内部的 table 中产生 hash碰撞的可能。
如果你的代码从头到尾只让 Thread 使用一个 ThreadLocal 对象,那么不好意思,ThreadLocal 的 hash碰撞跟你一毛钱关系没有。
Note:你也可以让多个 Thread 共用一个 ThreadLocal 对象,这并没有任何不妥。因为 ThreadLocal 只是 ThreadLocalMap 中的 key,key 往往是可重复的,这可以类比为标签,比如每个人都应该有一个“姓名”,但是这个“姓名”下面的真实属性(val)却不尽相同,很容易在人群中找到“姓名”不同的两个人。当然,如果你是在哈希表中存放所有用户的信息,那么key应当是每个用户信息中具有唯一性的那个属性。
ThreadLocal 类中封装了一个常量实例域:threadLocalHashCode,它通过一个静态方法 nextHashCade(); 来生成。通过这个静态方法,为不同的 ThreadLocal 对象派发一个 hashcode,这就是这个静态方法的作用。在 Java HashMap 中我们知道程序会对放入map中的 key 做一个 hash运算,这个 nextHashCade() 方法的作用是一样的。
Note:第一个 threadLocalHashCode 是 0,下一个是 0+0x61c88647,即步进 0x61c88647。
如果你的某一个线程池代码需要处理线程副本变量,并且使用到了多个 ThreadLocal 对象,当这些对象很多时,nextHashCade() 方法分发的 hashcode “碰巧”发生了碰撞,那么碰撞之后的下标寻找将在当前下标的基础上+1 去寻找新下标,即 线性探测法。而 HashMap 中的冲突解决策略是链表法,即用一个 Node.next 指针将各个元素串联到一个链表上。
3. ThreadLocal 的内存泄漏:
ThreadLocalMap 中的 Entry 是继承了 WeakReference 的,但只有 Entry 中的 kye 对象即 ThreadLocal 对象会被 WeakReference 关联上,Entry 中的 value 是没有被弱引用关联的。这就导致当 ThreadLocal 不再被线程引用之后,垃圾回收器对 Thread 中的 ThreadLocalMap 进行回收时可以回收掉 ThreadLocal,这样就会在 ThreadLocalMap 中产生 Entry 的 key 为 null 的 Entry。而 value 还被这个 ThreadLocalMap 的一些 Entry 关联着。就产生了所谓的“内存泄漏”。
因此 ThreadLocal 提供了 remove() 方法来释放这些 value,查看源码可以发现它调用 ThreadLocalMap 的 remove(ThreadLocal) 方法,清理所有 key == null 的 entry,而不是只清理一个。