我把 ThreadLocal 能问的,都写了(下)

简介: 我把 ThreadLocal 能问的,都写了(下)

所以说,如果通过 key 的哈希值得到的下标无法直接命中,则会将下标 +1,即继续往后遍历数组查找 Entry ,直到找到或者返回 null。

可以看到,这种 hash 冲突的解决效率其实不高,但是一般 ThreadLocal 也不会太多,所以用这种简单的办法解决即可。

至于代码中的expungeStaleEntry我们等下再分析,先来看下 ThreadLocalMap#set 方法,看看写入的怎样实现的,来看看 hash 冲突的解决方法是否和上面说的一致。


image.png


可以看到 set 的逻辑也很清晰,先通过 key 的 hash 值计算出一个数组下标,然后看看这个下标是否被占用了,如果被占了看看是否就是要找的 Entry ,如果是则进行更新,如果不是则下标++,即往后遍历数组,查找下一个位置,找到空位就 new 个 Entry 然后把坑给占用了。

当然,这种数组操作一般免不了阈值的判断,如果超过阈值则需要进行扩容。

上面的清理操作和 key 为空的情况,下面再做分析,这里先略过。

至此,我们已经分析了 ThreadLocalMap 的**核心操作 get 和 set **,想必你对 ThreadLocalMap 的原理已经从源码层面清晰了!

可能有些小伙伴对 key 的哈希值的来源有点疑惑,所以我再来补充一下 key.threadLocalHashCode的分析。


image.png


可以看到key.threadLocalHashCode其实就是调用 nextHashCode 进行一个原子类的累加。

注意看上面都是静态变量和静态方法,所以在 ThreadLocal 对象之间是共享的,然后通过固定累加一个奇怪的数字0x61c88647来分配 hash 值。

这个数字当然不是乱写的,是实验证明的一个值,即通过 0x61c88647 累加生成的值与 2 的幂取模的结果,可以较为均匀地分布在 2 的幂长度的数组中,这样可以减少 hash 冲突。

有兴趣的小伙伴可以深入研究一下,反正我没啥兴趣。


ThreadLocal 内存泄露之为什么要用弱引用


接下来就是要解决上面挖的坑了,即 key 的弱引用、Entry 的 key 为什么可能为 null、还有清理 Entry 的操作。

之前提到过,Entry 对 key 是弱引用,那为什么要弱引用呢?

我们知道,如果一个对象没有强引用,只有弱引用的话,这个对象是活不过一次 GC 的,所以这样的设计就是为了让当外部没有对 ThreadLocal 对象有强引用的时候,可以将 ThreadLocal 对象给清理掉。

那为什么要这样设计呢?

假设 Entry 对 key 的引用是强引用,那么来看一下这个引用链:


image.png

从这条引用链可以得知,如果线程一直在,那么相关的 ThreadLocal 对象肯定会一直在,因为它一直被强引用着。

看到这里,可能有人会说那线程被回收之后就好了呀。

重点来了!线程在我们应用中,常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着!

所以这条引用链需要弱化一下,而能操作的只有 Entry 和 key 之间的引用,所以它们之间用弱引用来实现。

与之对应的还有一个条引用链,我结合着上面的线程引用链都画出来:


image.png


另一条引用链就是栈上的 ThreadLocal 引用指向堆中的 ThreadLocal 对象,这个引用是强引用。

如果有这条强引用存在,那说明此时的 ThreadLocal 是有用的,此时如果发生 GC 则 ThreadLocal 对象不会被清除,因为有个强引用存在。

当随着方法的执行完毕,相应的栈帧也出栈了,此时这条强引用链就没了,如果没有别的栈有对 ThreadLocal 对象的引用,那么说明 ThreadLocal 对象无法再被访问到(定义成静态变量的另说)。

那此时 ThreadLocal 只存在与 Entry 之间的弱引用,那此时发生 GC 它就可以被清除了,因为它无法被外部使用了,那就等于没用了,是个垃圾,应该被处理来节省空间。

至此,想必你已经明白为什么 Entry 和 key 之间要设计为弱引用,就是因为平日线程的使用方式基本上都是线程池,所以线程的生命周期就很长,可能从你部署上线后一直存在,而 ThreadLocal 对象的生命周期可能没这么长。

所以为了能让已经没用 ThreadLocal 对象得以回收,所以 Entry 和 key 要设计成弱引用,不然 Entry 和 key是强引用的话,ThreadLocal 对象就会一直在内存中存在。

但是这样设计就可能产生内存泄漏。

那什么叫内存泄漏?就是指:程序中已经无用的内存无法被释放,造成系统内存的浪费。

当 Entry 中的 key 即 ThreadLocal 对象被回收了之后,会发生 Entry 中 key 为 null 的情况,其实这个 Entry 就已经没用了,但是又无法被回收,因为有 Thread->ThreadLocalMap ->Entry 这条强引用在,这样没用的内存无法被回收就是内存泄露。

那既然会有内存泄漏还这样实现?

这里就要填一填上面的坑了,也就是涉及到的关于 expungeStaleEntry即清理过期的 Entry 的操作。

设计者当然知道会出现这种情况,所以在多个地方都做了清理无用 Entry ,即 key 已经被回收的 Entry 的操作。

比如通过 key 查找 Entry 的时候,如果下标无法直接命中,那么就会向后遍历数组,此时遇到 key 为 null 的 Entry 就会清理掉,再贴一下这个方法:


image.png

这个方法也很简单,我们来看一下它的实现:


image.png


所以在查找 Entry 的时候,就会顺道清理无用的 Entry ,这样就能防止一部分的内存泄露啦!

还有像扩容的时候也会清理无用的 Entry:


image.png


其它还有,我就不贴了,反正知晓设计者是做了一些操作来回收无用的 Entry 的即可。


ThreadLocal 的最佳实践


当然,等着这些操作被动回收不是最好的方法,假设后面没人调用 get 或者调用 get 都直接命中或者不会发生扩容,那无用的 Entry 岂不是一直存在了吗?所以上面说只能防止一部分的内存泄露。

所以,最佳实践是用完了之后,调用一下 remove 方法,手工把 Entry 清理掉,这样就不会发生内存泄漏了!

void yesDosth {
  threadlocal.set(xxx);
  try {
    // do sth
  } finally {
    threadlocal.remove();
  }
}

这就是使用 Threadlocal 的一个正确姿势啦,即不需要的时候,显示的 remove 掉。

当然,如果不是线程池使用方式的话,其实不用关系内存泄漏,反正线程执行完了就都回收了,但是一般我们都是使用线程池的,可能只是你没感觉到。

比如你用了 tomcat ,其实请求的执行用的就是 tomcat 的线程池,这就是隐式使用。

还有一个问题,关于 withInitial 也就是初始化值的方法。

由于类似 tomcat 这种隐式线程池的存在,即线程第一次调用执行 Threadlocal 之后,如果没有显示调用 remove 方法,则这个 Entry 还是存在的,那么下次这个线程再执行任务的时候,不会再调用 withInitial 方法,也就是说会拿到上一次执行的值

但是你以为执行任务的是新线程,会初始化值,然而它是线程池里面的老线程,这就和预期不一致了,所以这里需要注意。


InheritableThreadLocal


这个其实之前文章写过了,不过这次竟然写了 threadlocal  就再拿出来。

这玩意可以理解为就是可以把父线程的 threadlocal 传递给子线程,所以如果要这样传递就用 InheritableThreadLocal ,不要用 threadlocal。

原理其实很简单,在 Thread 中已经包含了这个成员:


image.png


image.png


这里要注意,只会在线程创建的时会拷贝 InheritableThreadLocal 的值,之后父线程如何更改,子线程都不会受其影响。


最后


至此有关 ThreadLocal 的知识点就差不多了。

想必你已经清楚 ThreadLocal 的原理,包括如何实现 ThreadLocal ,为什么 key 要设计成弱引用,并且关于线程池中使用 ThreadLocal 的注意点等等。

其实本没打算写 ThreadLocal 的,因为最近在看 Netty ,所以想写一下 FastThreadLocal ,但是前置知识点是 ThreadLocal ,所以就干了一篇 ThreadLocal 。

消化了这篇之后,出去面试 ThreadLocal 算是没问题了吧,最后再留个小小的思考题。

那为什么 Entry 中的 value 不弱引用?

这个题目来自群友的一个面试题哈,想必看完这篇文章之后,这个题目难不倒你,欢迎留言区写出答案!

可以关注我,等我下篇的 ThreadLocal 进阶版哈


相关文章
|
存储 安全 Java
【ThreadLocal】
【ThreadLocal】
|
4月前
|
存储 Java
ThreadLocal应用及理解
ThreadLocal应用及理解
47 10
|
6月前
|
存储 Java 数据管理
ThreadLocal的使用
`ThreadLocal`是Java中的线程局部变量工具,确保每个线程都有自己的变量副本,互不干扰。适用于保持线程安全性数据和跨方法共享数据。基本用法包括创建实例、设置和获取值以及清除值。例如,创建ThreadLocal对象后,使用`.set()`设置值,`.get()`获取值,`.remove()`清除值。注意ThreadLocal可能引起内存泄漏,应适时清理,并谨慎使用以避免影响代码可读性和线程安全性。它是多线程编程中实现线程局部数据管理的有效手段。
89 10
|
6月前
|
存储 Java
ThreadLocal 有什么用
ThreadLocal 有什么用
54 0
|
存储 算法 安全
深入详解ThreadLocal
在我们日常的并发编程中,有一种神奇的机制在静悄悄地为我们解决着各种看似棘手的问题,它就是 ThreadLocal 。
21514 9
深入详解ThreadLocal
|
11月前
|
存储
ThreadLocal
ThreadLocal
53 0
|
缓存 安全 Java
浅谈ThreadLocal
浅谈ThreadLocal
149 0
|
存储 SQL Java
ThreadLocal的其他应用
request对象跟PageHelper
104 0
|
存储 分布式计算 安全
什么是ThreadLocal?
这篇文章是慕课网上一门免费课程《ThreadLocal》的观后总结。这门课将ThreadLocal讲得非常清晰易懂,又深入底层原理和设计思想,是我看过的最好的ThreadLocal的资料,现在把用自己的话,把它整理成文字版本。 总共预计产出四篇文章,这是第一篇。
267 3
|
存储 Java
ThreadLocal相关使用
ThreadLocal相关使用
192 0
ThreadLocal相关使用