ThreadLocal有哪些扩展实现?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。
回答话术
首先,由于 ThreadLocal 的数据是绑定到线程对象的,因此线程之间的 ThreadLocal 其实是彼此隔离的,所以当我们在父线程调用的子线程时,子线程是无法获取到父线程的数据的。
为此,JDK 提供了 ThreadLocal 的一个扩展实现 InheritableThreadLocal,它对应线程对象中的 inheritableThreadLocal 属性。当我们通过父线程创建一个子线程的时候,子线程中的 InheritableThreadLocal 将会把父线程中 InheritableThreadLocal 里的数据拷贝过来,从而实现父子线程参数的传递。
不过,由于 InheritableThreadLocal 的参数传递是在父线程创建子线程的时候进行的,因此当我们使用线程池时,由于线程池中的线程是提前创建好的,所以父线程的上下文参数自然也就无法传递到线程池中的线程。
针对这个问题,阿里开源了一个 TransmittableThreadLocal,它对 InheritableThreadLocal 做了增强。它提供了一套快照机制,结合它的任务装饰器,当线程池中的线程执行任务时,装饰器将会生成一个快照,然后再从主线程拷贝数据,当执行完任务以后,就再根据快照还原回执行任务前的状态。
除了阿里的 TransmittableThreadLocal 外,还有一个比较知名的就是 Netty 的 FastThreadLocal,它在实现上与 ThreadLocal 没有什么关系,是 Netty 针对 ThreadLocal 在哈希冲突下的性能问题而另外开发出来的替代品。Netty 让每个 FastThreadLocal 创建时都通过一个全局的 AtomicInteger 分配一个下标作为“哈希值”,此后当使用时,直接根据下标找到对应的槽位。这种方案通过一定程度上花费更多的内存,从而完全避免下标计算带来的性能开销与哈希冲突。
问题详解
1. InheritableThreadLocal
由于 ThreadLocal 是绑定在线程对象的,因此线程间的 ThreadLocal 实际上彼此隔离,当某个线程执行的过程中创建或调用了其他线程,其他线程是无法感知到主线程 ThreadLocal 中的数据的。
之前我们在看 ThreadLocal 源码的时候,就注意到线程对象除了 threadLocal 这个变量外,还有一个 inheritableThreadLocal,它其实就是 JDK 针对此类场景的一种处理措施。
JDK 提供了一个名为 InheritableThreadLocal 的 ThreadLocal 子类,当线程对象执行 init 方法时,它会调用 ThreadLocal.createInheritedMap 方法创建一个 InheritableThreadLocal 对象,随后它会将父线程的 inheritableThreadLocals 中的数据拷贝过来,依靠此实现父子线程的数据传递。
比如:
ThreadLocal<Object> tl = new InheritableThreadLocal();
tl.set("value");
// 第一次创建线程时,会将父线程的值复制到子线程
new Thread(() -> {
tl.get(); // = "value"
}).start();
不过,InheritableThreadLocal 也存在局限性,由于其只在线程初始化时拷贝一次数据,因此当使用线程池时,若线程已经启动,那么数据就无法传递进去。
比如:
// 预热线程池,提前创建好线程
Executor executor = Executors.newFixedThreadPool(1);
executor.execute(() -> {});
ThreadLocal<Object> tl = new InheritableThreadLocal<>();
tl.set("value");
executor.execute(() -> {
tl.get(); // = null
});
甚至,如果线程池中的线程是在主线程中启动的(比如创建核心线程),那么主线程中的数据将会污染线程池中的线程,从而导致出现“下一个请求中获取的用户数据是上一个请求遗留下来的”这种问题。
比如:
Executor executor = Executors.newFixedThreadPool(1);
ThreadLocal<Object> tl = new InheritableThreadLocal<>();
// 第一次创建线程时,会将父线程的上下文参数复制到子线程
tl.set("value");
executor.execute(() -> {
tl.get(); // = "value"
});
// 此后若线程不销毁,则子线程的上下文参数始终保持原样
tl.set("value2");
executor.execute(() -> {
tl.get(); // = "value"
});