面试官再问你 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微服务 入门 实战与进阶》作者, 公众号猿天地发起人。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容