前言
今天讲,ThreadLocal 本来想简单写点应用的,但是由于太晚了,原因在末尾。所以简短点,直接开门见山。
正文
ThreadLocal
ThreadLocal 对象呢可以提供线程局部变量,也就是说每个线程都拥有一份属于自己的副本变量。多个线程之间呢是互不干扰的。这是我们对 ThreadLocal 的第一印象。其原理呢,我们可以大概捋一下:
老版本的 ThreadLocal 设计:它会在 threadLocal 中维护一个大 map,所有的线程变量都会维护在一个 map 里。
Java8 呢就是每个线程对应的 Thread 对象内部都拥有一个 threadLocals 字段。
这个字段会指向堆中的一个 ThreadLocalMap。这个 map 呢存储的是以当前线程为 key,以当前线程相关联的数据为 value 的键值对。线程访问某个 ThreadLocal 对象 get 方法的时候会检测当前线程 map 内是否有以当前线程为 key 的 Entry 数据。如果没有的话就会调用 setInitialValue方法去创建一个 Entry,然后存放到这个 ThreadLocalMap 里面。每个线程都是独一无二的,这样就使线程之间互不干扰了。
现在我们大体清楚了 ThreadLocal 的大致内容,是不是觉得很精妙啊。其实,ThreadLocal 也是慢慢优化而来的,之前老版本的 ThreadLocal 设计它会在 threadLocal 中维护一个大 map,所有的线程变量都会维护在一个 map 里。那么你会发现如果线程很多的话这个 map 会很大,不利于维护。而 Java8 之后的版本中,每个线程只维护自己的数据。当线程被销毁的时候,线程对应的 ThreadLocalMap 就会在下一次 GC 的时候被回收了。
特别的 ThreadLocalMap
现在我们都了解了 ThreadLocal 它底层有一个 ThreadLocalMap。而这个 map 的结构呢和HashMap 几乎一样。初始大小为 16,加载因子 0.75,而且容量必须是 2 的 n 次方倍。为什么一定是 2 的 n 次方,其实是为了方便寻址。因为 2 的 n 次方 -1 后转换为二进制后末尾是一堆 1,如果按照这种二进制数按位与运算所得到的数一定大于等于0 且 小于这个二进制数的(0 ~ 16)。与 HashMap 不一样的点是它的扩容,当它容量到达 table.length * 0.75 的时候它不会立刻扩容,它还会进行一次 rehash。调用 rehash 算法,将整个散列表扫描,清理掉过期数据,重新整理散列表。具体详情看我下一篇源码分析。
每当提到 Map,我们一定会想到这个 Map 的哈希算法是怎么实现,遇到元素冲突是怎么处理的。ThreadLocalMap 中元素的 hashCode 和冲突处理非常有意思。首先这个这个 Map 不同于 HashMap,当 ThreadLocalMap 中元素发生冲突了它不会形成链表与红黑树,而是继续向后遍历,直到找到一个可以适合自己的桶位。举个例子:比如我一个线程存进来经过计算后它的桶位是 2,但是呢我们在 put 的时候发现这个 2 号桶位已经有人了。那么它就会向后查找,发现 3 号也有人了,再向后,发现 4 号位空着呢,那么它就存在 4 号位。
那么再来说它的 hashCode 值。
创建新的 ThreadLocal 对象时,会给当前对象分配一个 hash,用的就是 nextHashCode 方法。它里面还会加一个固定值 HASH_INCREMENT,黄金分割数,让哈希分配更均匀。比如我们初始化大小是 16,那么它连续分配四个元素分别为 table[0],table[4],table[8],table[12] 这样子。最后呢还会通过 key.threadLocalHashCode & (table.length - 1) 的方式去计算桶位。
聊了有意思的 ThreadLocalMap,不知道你有没有过这样的疑问,为啥不用自己写好的 HashMap 呢?首先啊,ThreadLocal 中的 Key 是固定类型 ThreadLocal,其次最主要的呢就是弱引用了,而 HashMap 的 key 是强引用,弱引用是不影响对象被回收的。线程消亡了,同样 ThreadLocalMap 也会被回收的。最后呢还有一个鸡肋的原因,ThreadLocalMap 它的写数据和查数据过程中都有清理过期数据的功能。这块我们下一篇源码篇会详细讲解。
我们发现它元素单位 Entry 是继承弱引用的。弱引用大家都知道,就是一旦 GC 开始了,那么它就会被回收。这样设计也是方便 GC 垃圾回收。
刁端小问题
什么时候 ThreadLocalMap 会初始化呢?
这里,ThreadLocal 是延迟初始化的,当第一次操作 ThreadLocal 的时候,即 get/set 方法会判断当前 ThreadLocalMap 是否为空,如果是则会调用 createMap 方法去创建并赋值。并且在当前线程里只会初始化一次。
讲讲扩容
它首先会创建一个新的数组,长度是当前散列表数组的两倍。迭代老数组,将其中的数量重新按照 hash 算法放入新的数组里面。迁移完数据后会更新散列表引用,指向这个新的数组,并且会计算下次扩容阈值。
结语
今天公司突然聚餐,而且工作又来无聊的任务了,耽误文章进度。本来打算出一篇概述,一篇源码的。现在已经是凌晨 一点半了,喝了酒回家都十点了,明天还要上班,源码明天写吧。唉,打工人不容易。
2021年03月17日01:30:30