从类的javadoc出发
想要深度了解一个类可以从 javadoc 出发,这里可藏着不少好东西,下面让我来带大家盘一盘 ThreadLocal 的 javadoc!
图片
从区域①可以看出 ThreadLocal 的用途:提供了线程纬度的局部变量。通俗来讲就是每一个线程操作自己的局部变量,线程之间互不干扰。
通过这段描述我们还可以发现官方是建议我们将 ThreadLocal 用作类中私有的静态成员变量。
区域②是官方为我们提供了一个小 demo,模拟了为每个线程生成线程 id 的场景,并且这个 id 在第一次调用 ThreadId.get 时被分配,并在后续调用中保持不变。
可以看到这个 demo 中将 ThreadLocal 对象使用 private static final 修饰,这也正是官方所建议的。
区域③官方着重强调了局部变量与线程的关系,一旦线程销毁,局部变量也会被垃圾回收器回收掉。
常用API分析
局部变量是如何存储的?为何又会随着线程的销毁而销毁?在这个过程中 ThreadLocal 又充当着怎样的角色?源码之下没有秘密!
图片
通过 idea 侧边栏提供的 Structure 模块可以看出 ThreadLocal 类中的方法并不多,在使用中可以用到的也就圈出来的这几个。
我们将这几个方法玩明白,上面的问题也就迎刃而解了。
initialValue方法
首先我们看一下 initialValue 方法。
图片
通过源码我们可以看出这个方法的访问修饰符被设置为了 protected 类型的,意味着这个方法只能被同包及其子类访问,并且这个方法的实现是直接返回了 null,可以推断出这个方法应该是用作模板方法(钩子函数),并且结合方法名可以判断出这个方法作用是初始化值。
而这一切在方法的 javadoc 都有所描述,所以在看源码的时候一定不可以忽略 javadoc,这里含有大量有用信息,即使使用翻译软件也一定要含泪看完。
在实践中想要利用这个方法,必须对 ThreadLocal 进行子类化,并重写此方法。通常有两种方式,其中一种是开篇的 javadoc 中使用的匿名内部类写法。
图片
而匿名内部类写法,可以用 ThreadLocal 提供的 withInitial 方法进行等效替换,个人更倾向于使用 withInitial 配合 lambda 表达式的写法,可以使得代码更加简洁清晰。
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> nextId. getAndIncrement());
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(nextId::getAndIncrement);
那么 initialValue 方法将在什么时候调用呢?先给出结论:在当前线程对象中以 ThreadLocal 对象为 key 的局部变量值不存在时,调用 get 请求将会触发初始化逻辑。
get方法
下面我们来看一下 get 方法,通过分析 get 方法我们就可以知道局部变量是如何存储的。
图片
从源码中可以看到首先是获取当前的线程对象,然后通过 getMap 方法传入当前 Thread 对象获取到了一个 ThreadLocalMap 对象 map。
并判断当前 map 是否为 null,如果不为 null 则调用 map 的 getEntry 方法,并且传入了 this 对象,在此刻 this 对象就是 ThreadLocal 对象。
getEntry 方法返回了 Entry 对象 e,如果 e 不为 null 则返回 e 的 value 成员变量,并将其转换为我们定义好的泛型 T 进行返回。这里的 e.value 就是我们所说的局部变量。
图片
我们来看一下 Entry 类的定义,通过源码可以看出 Entry 类继承了 WeakReference,并将引用字段作为键(始终是 ThreadLocal 对象),value 则定义为 Object 类型。关于弱引用问题将在下一期分析内存泄露问题时进行展开讨论。
如果 map 为 null 或者对象 e 为 null,都将调用 setInitialValue 方法进行初始化,而 setInitialValue 方法将会调用上述我们重写的 initialValue 方法获取局部变量的值,进行初始化操作。
图片
此时我们已经可以确定局部变量存储在 ThreadLocalMap 对象中。那么 ThreadLocalMap 对象又从何获取的呢,通过点进 getMap 方法源码(如下图)我们可以发现,ThreadLocalMap 是 Thread 类的成员变量,也就是说存储在 Thread 对象中。
图片
点进 Thread 源码(如下图)我们就可以看到类型为 ThreadLocalMap 的成员变量 threadLocals,并且它的初始值是 null。
图片
那么在这个过程中 ThreadLocal 对象充当了什么样的角色呢?其实 ThreadLocal 对象的作用就相当于 HashMap 中 key 的作用。我们点进以 ThreadLocal 对象作为参数的 getEntry 方法,进行进一步的分析。
图片
通过 Entry e = table[i]我们发现 Entry 对象取自 table 这个数组,而数组 table 是 ThreadLocalMap 的类的成员变量。
图片
数组坐标 i 通过表达式key.threadLocalHashCode & (table.length - 1)计算而来,这个表达式在 table.length 是 2 的幂次方时等同于key.threadLocalHashCode % table.length操作,具体论证的过程大家可以百度一下。
如果通过坐标 i 获取的 Entry 对象的 key 和当前的 ThreadLocal 对象相等,则证明当前 Entry 对象确实是存储了当前线程当前 ThreadLocal 对象的局部变量。
如果不等,说明发生了哈希冲突,则调用 getEntryAfterMiss 方法继续搜索,直到搜索到当前 ThreadLocal 对象对应的 Entry,或者未搜索到返回 null。
图片
在 getEntryAfterMiss 方法中,我们可以看到搜索的过程中调用了 nextIndex 方法进行获取下一次搜索的索引,nextIndex 方法的逻辑很简单就是对 i 进行递增,如果等于了容量值 len 则从 0 继续遍历。
图片
通过搜索逻辑我们可以推断出 ThreadLocalMap 解决哈希冲突采用的是线性探测法,而 HashMap 在碰到哈希冲突时采用的是拉链法,这一点要区别记忆。
此时我们已经可以抽离出一条引用链 Thread->ThreadLocalMap->Entry[]->Entry->value(局部变量),在这条引用链上都是强引用。
我们再来分析一下为何局部变量为什么会随着线程的销毁而销毁呢?
JVM 垃圾回收机制默认采取的是可达性分析算法。在这条强引用链中除了 value 调用链中的其他引用都是当前对象的唯一引用。
一旦这条引用链的根 Thread 对象被回收,那么其他对象都将不可达,都将被垃圾回收器所回收。
而局部变量如果没有被其他对象所引用也将不可达,从而被销毁。
set方法
下面我们来看一下 set 方法的源码,比较有意思的是,竟然和 setInitialValue 方法中的一段逻辑一毛一样,不知道编写时为什么没有在 setInitialValue 方法中直接调用 set 方法。
图片
set 方法在执行时根据 ThreadLocalMap 对象是否为 null,分别进行赋值及初始化两种不同处理逻辑。
这里我们先看一下 ThreadLocalMap 对象为 null 时的 createMap 方法的逻辑,这样更有助我们理解。
图片
在 createMap 方法内部调用了 ThreadLocalMap 两个参数的构造函数,并将返回的对象赋值给了当前线程对象的成员变量 threadLocals。
我们查看 ThreadLocalMap 的构造方法可以发现很多关键信息。
图片
其中 ThreadLocalMap 的初始容量被设置为了 16。
图片
并且在构造方法的最后调用了 setThreshold 方法,该方法用于设置扩容的阈值,这个阈值为当前容量的 2 / 3。
图片
当 ThreadLocalMap 对象不为 null 时将会调用 ThreadLocalMap 的 set 方法。
图片
从方法中我们可以看出,首先依旧是根据 ThreadLocal 对象计算出索引 i,然后根据当前索引值获取 Entry 对象。
如果 Entry 对象不为 null 则会进行下列几种判断,如果 key 是当前 ThreadLocal 对象,则将旧值替换掉。
如果当前 key 为 null,则说明当前 Entry 对象已经过时,则调用替换过时 Entry 的 replaceStaleEntry 方法,这个方法将在下一期进行展开讨论。
如果 key 不为 null 且不等于当前 ThreadLocal 对象则进行下一轮遍历。
如果上述逻辑未能找到当前 ThreadLocal 对象对应的 Entry 对象,且在这个过程中没有过时的 Entry 对象供替换,则生成一个新的 Entry 对象放置在当前索引 i 位置(经过上述遍历索引 i 已经定位在了一个 Entry 对象为 null 的位置)。
最后再根据!cleanSomeSlots(i, sz) && sz >= threshold判断一下是否需要进行扩容操作。
其中 cleanSomeSlots 方法的作用是向下执行有限次数的扫描,看看有没有过时的 Entry 对象可供清理,如果清理了任何个数 Entry 对象将返回 true,则此时一定不需要扩容,如果没有清理任何 Entry 对象则需要判断一下当前 ThreadLocalMap 的大小是否达到了扩容阈值。
remove方法
最后我们来看一下 remove 方法,remove 方法的作用是删除当前线程以当前 ThreadLocal 对象为 key 的局部变量值。
图片
通过源码我们可以看出 ThreadLocal 的 remove 方法的核心逻辑是调用了 ThreadLocalMap 的 remove 方法。
图片
ThreadLocalMap 的 remove 方法的逻辑也很清晰,根据 ThreadLocal 对象搜索对应的 Entry 对象,如果搜索到则将 Entry 对象通过 Reference 的 clear 方法设置为过时,最后调用 expungeStaleEntry 方法将过时的 Entry 条目进行清理,expungeStaleEntry 也将在下一期进行展开讨论。
总结
最后我们再来简单总结一下,首先每一个线程对象中都存储了一个 ThreadLocalMap 对象,ThreadLocalMap 对象以 ThreadLocal 对象作为 key 存储值,这个值就是我们所说的局部变量。
但是在设计的过程中并没有直接暴露给我们操作 ThreadLocalMap 的 API,所以在这个过程中我们需要 ThreadLocal 对象作为桥梁,ThreadLocal 类包含 initialValue、get、set、remove 方法。
其中 initialValue 方法用于提供初始化 ThreadLocalMap 对象中以当前 ThreadLocal 对象为 key 的局部变量的值。
get 方法用于获取当前线程以当前 ThreadLocal 对象为 key 的局部变量,如果当前局部变量的未初始化,则使用 initialValue 返回的值作为局部变量的值进行初始化操作。
set 方法用于为当前线程以当前 ThreadLocal 对象为 key 的局部变量设置值,新值将会覆盖旧值。
remove 方法用于删除当前线程以当前 ThreadLocal 对象为 key 的局部变量值。