Loading...
墨滴

cxytiandi

2021/03/25  阅读:32  主题:极客黑

面试官再问你 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<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal();
    // ...............
}

场景四:ThreadLocal 实现同一线程下多个类之间的数据传递

在 Spring Cloud Zuul 中,过滤器是必须要用的。用过滤器我们可以实现权限认证,日志记录,限流等功能。

过滤器有多个,而且是按顺序执行的。过滤器之前要透传数据该如何处理?

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<String, Object> {
    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
        @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 有什么需要注意的吗?

  • 避免跨线程异步传递,虽然有解决方案,文末介绍了方案
  • 使用时记得及时 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<T> extends ThreadLocal<T> {
    /**
     * 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.
     * <p>
     * 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<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    Object value = 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 微服务 入门 实战与进阶》作者, 公众号猿天地发起人。

cxytiandi

2021/03/25  阅读:32  主题:极客黑

作者介绍

cxytiandi