面试官再问你|面试官再问你 ThreadLocal,就这样狠狠 “怼” 回去!

本文大纲

  1. 用过ThreadLocal吗?在什么场景下会使用ThreadLocal
  2. 讲讲ThreadLocal的原理吧!
  3. 使用ThreadLocal有什么需要注意的吗?
  4. 有什么方式能提高ThreadLocal的性能吗?
  5. 如何将ThreadLocal的数据传递到子线程中?
  6. 线程池中如何实现ThreadLocal的数据传递?
用过ThreadLocal吗?在什么场景下会使用ThreadLocal。 这个回答一定要足够自信:必须用过啊,无论是在平时的业务开发过程中会用到,其他很多三方框架中也都用到了ThreadLocal。
如果你回答没用过,很有可能就凉凉了,因为ThreadLocal在很多场景都能用到,假如实在没用过也不要没信心,看完这篇文章你就知道如何回答了。
场景一:ThreadLocal+MDC实现链路日志增强
日志增强之前也写过一篇文章,讲解了实现的功能,细节没有讲,可以看看下面这篇文章了解。
文章:有了链路日志增强,排查Bug小意思啦!
比如我们需要在整个链路的日志中输出当前登录的用户ID,首先就得在拦截器获取过滤器中获取用户ID,然后将用户ID进行存储到ThreadLocal。
然后再层层进行透传,如果用的Dubbo,那么就在Dubbo的Filter中进行传递到下一个服务中。问题来了,在Dubbo的Filter中如何获取前面存储的用户ID呢?
答案就是ThreadLocal。获取后添加到MDC中,就可以在日志中输出用户ID。
场景二:ThreadLocal实现线程内的缓存,避免重复调用
缓存这块就不重复讲了,之前有单独写过文章,大家直接看之前的文章就可以了。
文章:简直骚操作,ThreadLocal还能当缓存用
场景三:ThreadLocal实现数据库读写分离下强制读主库
首先你的项目中要做了读写分离,如果有对读写分离不了解的同学可以查看这篇文章:读写分离
某些业务场景下,必须保证数据的及时性。主从同步有延迟,可以使用强制读主库来保证数据的一致性。
在Sharding JDBC中,有提供对应的API来设置强制路由到主库,具体代码如下:
HintManager hintManager = HintManager.getInstance(); hintManager.setMasterRouteOnly();

HintManager中就使用了ThreadLocal来存储相关信息。这样就可以实现在业务代码中设置路由信息,在底层的数据库路由那块获取信息,实现优雅的数据传递。
public final class HintManager implements AutoCloseable { private static final ThreadLocal HINT_MANAGER_HOLDER = new ThreadLocal(); // ............... }

场景四:ThreadLocal实现同一线程下多个类之间的数据传递
在Spring Cloud Zuul中,过滤器是必须要用的。用过滤器我们可以实现权限认证,日志记录,限流等功能。
【面试官再问你|面试官再问你 ThreadLocal,就这样狠狠 “怼” 回去!】过滤器有多个,而且是按顺序执行的。过滤器之前要透传数据该如何处理?
Zuul中已经提供了RequestContext来实现数据传递,比如我们在进行拦截的时候会使用下面的代码告诉负责转发的过滤器不要进行转发操作。
RequestContext.getCurrentContext().setSendZuulResponse(false);

RibbonRoutingFilter中就可以通过RequestContext获取对应的信息。
@Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null && ctx.sendZuulResponse()); }

RequestContext中就用了ThreadLocal。
public class RequestContext extends ConcurrentHashMap { protected static final ThreadLocal threadLocal = new ThreadLocal() { @Override protected RequestContext initialValue() { try { return contextClass.newInstance(); } catch (Throwable e) { throw new RuntimeException(e); } } }; // ......................... }

讲讲ThreadLocal的原理吧! ThreadLocal在使用的时候是单独创建对象的,更像一个全局的容器。但是大家有没有想过一个问题,就是为啥要设计ThreadLocal这个类,而不使用HashMap这样的容器类?
ThreadLocal本质上是要解决线程之间数据的隔离,以达到互不影响的目的。如果我们用一个Map做数据存储,Key为线程ID, Value为你要存储的内容,其实也是能达到隔离的效果。
没错,效果是能达到,但是性能就不一定好了,涉及到多个线程进行数据操作。如果你不看ThreadLocal的源码,你肯定也会以为ThreadLocal就是这么实现的。
ThreadLocal在设计这块很巧妙,会在Thread类中嵌入一个ThreadLocalMap,ThreadLocalMap就是一个容器,用于存储数据的,但它在Thread类中,也就说存储的就是这个Thread类专享的数据。
原本我们以为的ThreadLocal设置值的代码:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocal.put(t.getId(), value); }

正在的设置值的代码:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }

可以看到,先是获取当前线程对象,然后从当前线程中获取线程的ThreadLocalMap,值是添加到这个ThreadLocalMap中的,key就是当前ThreadLocal的对象。
从使用的API看上去像是把值存储在了ThreadLocal中,其实值是存储在线程内部,然后关联了对应的ThreadLocal,这样通过ThreadLocal.get时就能获取到对应的值。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }

来张图感受下:
面试官再问你|面试官再问你 ThreadLocal,就这样狠狠 “怼” 回去!
文章图片
使用ThreadLocal有什么需要注意的吗?
  • 避免跨线程异步传递,虽然有解决方案,文末介绍了方案
  • 使用时记得及时remove, 防止内存泄露
  • 注释说明使用场景,方便后人
  • 对性能有极致要求可以参考开源框架的做法,用一些优化后的类,比如FastThreadLocal
有什么方式能提高ThreadLocal的性能吗? 这个问题其实是考察你对其他的一些框架的了解,因为在一些开源的框架中也有使用ThreadLocal的场景,但是这些框架为了让性能更好,一般都会做一些优化。
比如Netty中就重写了一个FastThreadLocal来代替ThreadLocal,性能在一定场景下比ThreadLocal更好。
性能提升主要表现在如下几点:
  • FastThreadLocal操作数据的时候,会使用下标的方式在数组中进行查找来代替ThreadLocal通过哈希的方式进行查找。
  • FastThreadLocal利用字节填充来解决伪共享问题。
其实除了Netty中对ThreadLocal进行了优化,自定义了FastThreadLocal。在其他的框架中也有类似的优化,比如Dubbo中就InternalThreadLocal,根据源码中的注释,也是参考了FastThreadLocal的设计,基本上差不多。
如何将ThreadLocal的数据传递到子线程中? InheritableThreadLocal可以将值从当前线程传递到子线程中,但这种场景其实用的不多,我相信很多人都没怎么听过InheritableThreadLocal。
那为什么InheritableThreadLocal就可以呢?
InheritableThreadLocal这个类继承了ThreadLocal,重写了3个方法,在当前线程上创建一个新的线程实例Thread时,会把这些线程变量从当前线程传递给新的线程实例。
public class InheritableThreadLocal extends ThreadLocal { /** * Computes the child's initial value for this inheritable thread-local * variable as a function of the parent's value at the time the child * thread is created.This method is called from within the parent * thread before the child is started. * * This method merely returns its input argument, and should be overridden * if a different behavior is desired. * * @param parentValue the parent thread's value * @return the child thread's initial value */ protected T childValue(T parentValue) { return parentValue; } /** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * Create the map associated with a ThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the table. */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }

通过上面的代码我们可以看到InheritableThreadLocal 重写了childValue, getMap,createMap三个方法,当我们往里面set值的时候,值保存到了inheritableThreadLocals里面,而不是之前的threadLocals。
关键的点来了,为什么当创建新的线程时,可以获取到上个线程里的threadLocal中的值呢?原因就是在新创建线程的时候,会把之前线程的inheritableThreadLocals赋值给新线程的inheritableThreadLocals,通过这种方式实现了数据的传递。
源码最开始在Thread的init方法中,如下:
if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

createInheritedMap如下:
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }

赋值代码:
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal key = (ThreadLocal) e.get(); if (key != null) { Object value = https://www.it610.com/article/key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }
线程池中如何实现ThreadLocal的数据传递? 如果涉及到线程池使用ThreadLocal, 必然会出现问题。首先线程池的线程是复用的,其次,比如你从Tomcat的线程到自己的业务线程,也就是跨线程池了,线程也就不是之前的那个线程了,也就是说ThreadLocal就用不了,那么如何解决呢?
可以使用阿里的ttl来解决,之前我也写过一篇文章,可以查看:Spring Cloud中Hystrix 线程隔离导致ThreadLocal数据丢失
贴上ttl的链接:https://github.com/alibaba/transmittable-thread-local
ttl是基于代码方式的改造,下面再给大家介绍一种不用改造代码的方式,基于Java Agent来实现的,牛的一批。
链接:https://github.com/Nepxion/DiscoveryAgent
关于作者:尹吉欢,简单的技术爱好者,《Spring Cloud微服务-全栈技术与案例解析》, 《Spring Cloud微服务 入门 实战与进阶》作者, 公众号猿天地发起人。

    推荐阅读