大家好,我是yes。
之前连续写了几篇关于 ThreadLocal 的文章,几乎把所有和 ThreadLocal 有关的知识点及其衍生的类都讲了,现在来做个汇总。
这篇的主要目的是把相关的点串起来讲一遍,关于细节还是得看看之前写的那几篇文章,文末会贴。
话不多说发车。

为什么需要 ThreadLocal

原因在于我们有这个需求:即让每个线程都有属于自己的本地资源,避免多线程之间的共享
我们的 web 服务通常部署在 Tomcat 上,而 Tomcat 默认用线程池处理请求,每个线程会处理一个请求。
这个请求会跨越很多代码,例如 Tomcat 到 Spring ,再到我们的业务代码中。这其中可能都需要识别这个请求的用户信息,例如用来做鉴权等操作。
怎么做?在所有方法的请求入参里都写一个用户信息的入参?这一点都不实际,你控制不了这么多方法
所以为了满足这个需求,自然而然的我们会想到把当前用户的信息存储在当前操作的线程里,这样这个请求需要经历的所有方法,都可以从当前线程内拿出当前的用户信息,非常地便捷。
可能有人会说,我也可以在一个地方统一存储所有的用户信息,需要的时候去拿就好了。这样确实可以,但是这就变成多线程去访问一个共享变量,为了保证线程安全,那不得不把这个资源上锁,我们知道,上锁了这性能这就下来了。
所以将一些本可以不需要竞争的资源本地化,这就是 ThreadLocal 的作用。当然从上面的说法也可以看出 ThreadLocal 也便于传参呀~

内存泄漏问题

说到 ThreadLocal  不可避免老引出内存泄漏问题。
ThreadLocal  和线程的关系如下图所示:
每个线程内部有个 threadLocalMap,map 里面存储的 key 是 threadLocal 对象,这样调用 threadLocal .get就可以根据当前线程找到本地的 map ,然后根据调用的 threadLocal 对象找到对应的 value。
在实现上 threadLocalMap 是一个由 Entry 对象组成的数组,Entry 对 key 的引用为弱引用,对 value 的引用为强引用。
整体相关的对象引用链如下:
基于上面的图,我们来看看谈到 threadLocal 经常说的内存泄漏指的是什么。
由于线程池内线程生命周期较长,所以图中下方的那条强引用链会一直存在,而图上方的强引用链随着方法的调用结束出栈之后就不复存在了,所以当前的 threadLocal 对象只有一条弱引用存在(key的弱引用)。
如果发生 gc ,在内存不足的时候  threadLocal 对象就会被回收,这样就会残留无用的 Entry 在线程对象中(key都没了,根本访问不到 value,所以无用)。这就是所说的内存泄漏(残留了无用的 Entry 无法回收)。

那既然会有内存泄漏为什么还这样实现

就是因为 key 对 threadLocal 对象之间是弱引用,这样在栈上没有 threadLocal 引用这个强引用之后(你可以认为之后不会在用这个 threadlocal 对象),threadLocal 对象才得以被回收。
如果 key 对 threadLocal 对象之间是强引用,那么无用的 threadLocal 对象就无法被回收了,这其实造成了更大的内存泄漏。
设计者知晓会出现这种情况,所以在多个地方都做了清理无用 Entry 的操作。
比如通过 key 查找 Entry 的时候,如果下标无法直接命中,那么就会向后遍历数组,此时遇到 key 为 null 的 Entry 就会清理掉,还有扩容的时候,等等。
当然,最佳实践是在用完之后就手动把它 remove 掉,这样就避免了内存泄漏的存在。
void
 yesDosth {

 threadlocal.set(xxx);

try
 {

// do sth
 } 
finally
 {

  threadlocal.remove(); 
//手动清理
 }

}

在一定场景下,ThreadLocal 的短板

上面提到 threadLocalMap 是由 Entry 数组实现的,定位 threadlocal 是数组的哪个下标是通过 hash 算法来实现的,而 hash 算法会产生冲突,threadLocalMap 的 hash 冲突解决办法是用线性探测法,也就是发现冲突了,那么就把下标往后一位,直到找到空位。
具体冲突解决过程,如图所示:
在追求极致性能的场景下,这样的实现效率低。极端点如果在第一个位置发生冲突且此时数组几乎满了,那么就可能需要一直往后遍历整个数组来查找空位,这性能就不太理想。
所以 Netty 基于 ThreadLocal 造了一个 FastThreadLocal。
然后我们还发现 ThreadLocal 无法将本地资源传递给子线程。在有些场景,当父线程 new 一个子线程的时候,希望把它的 ThreadLocal 继承给子线程。
而 ThreadLocal 不支持这个操作,还好 jdk 提供了 InheritableThreadLocal 。
InheritableThreadLocal 提供了往子线程传递本地资源的功能,但它有局限性,也就是在父线程里只有通过 new 一个子线程才能传递本地资源,因为传递的操作是在子 Thread 初始化的时候发生的。而往往在平时我们用的都是线程池。
Runnable task = 
new
 Runnable....;

executorService.submit(task); 
//提交任务至线程池中
也就是父线程往往直接提交任务到线程池中,不会直接 new 一个子线程来执行任务,所以 InheritableThreadLocal 满足不了这个需求。
这时候就是 TransmittableThreadLocal 登场的时候了,它是由阿里开源的,支持往线程池内的线程传递 ThreadLocal
大概实现逻辑,如下图所示:
两个短板分别由两个扩展类补充,可以看到它们补充的一个是性能,一个是功能。

FastThreadLocal 和 TransmittableThreadLocal

FastThreadLocal 主要是补充 threadlocal 性能上的短板,追求性能的极致,优化了 hash 冲突,为每一个新建的 FastThreadLocal 对象都赋予了一个唯一的下标。
这样去 get 的时候,可以直接通过下标去数组查找对应的 value,不需要 hash 计算,也不会产生冲突,更加地高效。
TransmittableThreadLocal  主要是补充 threadlocal 功能上的不足
通过封装,在任务新建时把本地资源存于任务内部,在线程池执行任务时,在任务中获得保存的本地资源数据并塞至线程的上下文中,达到往线程池中传递 ThreadLocal 的功能。
更为惊艳的是提供了 Java Agent 来修饰 JDK 的线程池实现类,使得在代码中几乎可以无感知的实现往线程池中传递 ThreadLocal 的功能。
关于 FastThreadLocal 和 TransmittableThreadLocal  的细节这里不再赘述,可以看我之前的文章。

最后

至此,关于 ThreadLocal 的关键点应该都串起来了,不过也只是一个简单的汇总,更多的看我之前写的这三篇:

我是yes,从一点点到亿点点,我们下篇见。
继续阅读
阅读原文