前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文理解ThreadLocal

一文理解ThreadLocal

作者头像
全菜工程师小辉
发布2021-07-23 16:46:05
3410
发布2021-07-23 16:46:05
举报

本文讲解ThreadLocal、InheritableThreadLocal与TransmittableThreadLocal。

有关本文的实验代码,可以查看文末补充:“比较一下ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在线程池复用线程的情况下的执行情况”。

ThreadLocal

ThreadLocal的使用场景

  1. 分布式跟踪系统
  2. 日志收集记录系统上下文
  3. Session级Cache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

举例:

  1. Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
  2. 某些业务场景下,需要强制读主库来保证数据的一致性。在Sharding-JDBC中使用了ThreadLocal来存储相关配置信息,实现优雅的数据传递。
  3. Spring Cloud Zuul用过滤器可以实现权限认证,日志记录,限流等功能,多个过滤器之间透传数据,底层使用了ThreadLocal。
  4. 在整个链路的日志中输出当前登录的用户ID,首先就得在拦截器获取过滤器中获取用户。ID,然后将用户ID进行存储到slf4j的MDC对象(底层使用ThreadLocal),然后进行链路传递打印日志。

ThreadLocal的结构

  1. ThreadLocal的get()、set()方法,实际操作的都是Thread.currentThread(),即当前线程的threadLocals变量。
  2. threadLocals变量包含了一个map成员变量(ThreadLocalMap)。
  3. ThreadLocalMap的key为当前ThreadLocal, value为set的值。

相同的key在不同的散列表中的值必然是独立的,每个线程都是在各自的散列表中执行操作,如下图所示:

ThreadLocal的set方法:

代码语言:javascript
复制
public void set(T value) {
    //currentThread是个native方法,会返回对当前执行线程对象的引用。
    Thread t = Thread.currentThread();
    //getMap 返回线程自身的threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //把value set到线程自身的ThreadLocalMap中了
        map.set(this, value);
    } else {
        //线程自身的ThreadLocalMap未初始化,则先初始化,再set
        createMap(t, value);
    }
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal在set的时候,没有进行相应的深拷贝,所以ThreadLocal要想做线程隔离,必须是基本类型或者是Runable实现类的局部变量。

ThreadLocal造成内存泄漏

ThreadLocalMap内部Entry:

代码语言:javascript
复制
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

从代码中看到,Entry继承了WeakReference,并将ThreadLocal设置为了WeakReference,value设置为强引用。也就是:当没有强引用指向ThreadLocal变量时,它可被回收。

内存泄漏风险:ThreadLocalMap维护ThreadLocal变量与具体实例的映射,当ThreadLocal变量被回收后(变为null),无法路由到ThreadLocalMap。而该Entry还是在ThreadLocalMap中,从而这些无法清理的Entry,会造成内存泄漏。

所以,在使用ThreadLocal的时候,会话结束前务必使用ThreadLocal.remove方法(remove方法会将Entry的value及Entry自身设置为null并进行清理)。

ThreadLocal的最佳实践

  1. ThreadLocal使用时必须显式地调用remove方法来避免内存泄漏。
  2. ThreadLocal对象建议使用static修饰。这样做的好处是可以避免重复创建对象所导致的浪费(类第一次被使用时装载,只分配一块存储空间)。坏处是正好形成内存泄漏所需的条件(延长了ThreadLocal的生命周期,因此需要remove方法兜底)。
  3. 注释说明使用场景。
  4. 对性能有极致要求可以参考开源框架优化后的类,比如Netty的FastThreadLocal、Dubbo的InternalThreadLocal等。

InheritableThreadLocal

在全链路跟踪框架中,Trace信息的传递功能是基于ThreadLocal的。但实际业务中可能会使用异步调用,这样就会丢失Trace信息,破坏了链路的完整性。

此时可以使用JDK实现的InheritableThreadLocal,但它只支持父子线程间传递信息(例如:paramstream、new Thread等)。

Thread内部为InheritableThreadLocal开辟了一个单独的ThreadLocalMap(与ThreadLocal并列的成员变量)。在父线程创建一个子线程的时候,会检查这个ThreadLocalMap是否为空,不为空则会浅拷贝给子线程的ThreadLocalMap。

从类的继承层次来看,InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重写了getMap、createMap方法,整体流程与ThreadLocal保持一致。

Thread的init相关逻辑如下:

代码语言:javascript
复制
if (parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

需要注意的是拷贝为浅拷贝。

TransmittableThreadLocal

InheritableThreadLocal可以在父线程创建子线程的时候将ThreadLocal中的值传递给子线程,从而完成链路跟踪框架中的上下文传递。

但大部分业务应用都会使用线程池,这种复用线程的池化场景中,线程池中的线程和主线程并不都是父子线程的关系,不能直接使用InheritableThreadLocal。

例如从Tomcat的线程(池化)提交task到业务线程池,就不能直接使用InheritableThreadLocal。

Transmittable ThreadLocal(简称TTL)是阿里开源的库,继承了InheritableThreadLocal,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。

TransmittableThreadLocal实现原理

InheritableThreadLocal不支持池化线程提交task到业务线程池的根本原因是,父线程创建子线程时,子线程InheritableThreadLocal只会复制一次环境变量。要支持线程池中能访问提交任务线程的本地变量,只需要在线程向线程池提交任务时复制父线程的上下环境,那在线程池中就能够访问到父线程中的本地变量,实现本地环境变量在线程池调用中的透传。

源码见于参考文档1,README有很详细的讲解,核心源码也不难,建议看看。

此外,项目引入TTL的时候,可以使用Java Agent植入修饰代码,修改runnable或者callable类,可以做到对应用代码无侵入(这个在README也有相关讲解)。

补充说明

ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal在线程池复用线程的情况下的执行情况如下:

1.线程局部变量为基础类型

1.1 ThreadLocal

代码语言:javascript
复制
class TransmittableThreadLocalTest1 {
    static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    static ExecutorService executorService =
            Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开启");
        threadLocal.set(1);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.set(2);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            //[没有读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.set(3);
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);
        //依旧读取的是 2
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    }
}

输出结果为:

代码语言:javascript
复制
主线程开启
主线程读取本地变量:1
子线程读取本地变量:null
主线程读取本地变量:2
子线程读取本地变量:null
子线程读取本地变量:3
主线程读取本地变量:2

1.2 InheritableThreadLocal

代码语言:javascript
复制
class TransmittableThreadLocalTest2 {
    static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
    static ExecutorService executorService =
            Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开启");
        threadLocal.set(1);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.set(2);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            //[没有读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.set(3);
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);
        //依旧读取的是 2
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    }
}

输出结果为:

代码语言:javascript
复制
主线程开启
主线程读取本地变量:1
子线程读取本地变量:1
主线程读取本地变量:2
子线程读取本地变量:1
子线程读取本地变量:3
主线程读取本地变量:2

1.3 TransmittableThreadLocal

代码语言:javascript
复制
class TransmittableThreadLocalTest3 {
    static ThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
    static ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开启");
        threadLocal.set(1);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.set(2);
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            //[读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.set(3);
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);
        //依旧读取的是 2
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    }
}

输出结果为:

代码语言:javascript
复制
主线程开启
主线程读取本地变量:1
子线程读取本地变量:1
主线程读取本地变量:2
子线程读取本地变量:2
子线程读取本地变量:3
主线程读取本地变量:2

2.线程局部变量为类对象

首先定义一个数据类:

代码语言:javascript
复制
@Data
@AllArgsConstructor
class UserSession{
    String uuid;
    String nickname;
}

2.1 ThreadLocal

代码语言:javascript
复制
class TransmittableThreadLocalTest4 {
    static ThreadLocal<UserSession> threadLocal = new ThreadLocal<>();
    static ExecutorService executorService =
            Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开启");
        threadLocal.set(new UserSession("001","hello"));
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.get().setNickname("world");
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            //[没有读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.get().setNickname("Java");
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);
        //依旧读取的是 world
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    }
}

输出结果为:

代码语言:javascript
复制
主线程开启
主线程读取本地变量:UserSession(uuid=001, nickname=hello)
子线程读取本地变量:null
主线程读取本地变量:UserSession(uuid=001, nickname=world)
子线程读取本地变量:null
主线程读取本地变量:UserSession(uuid=001, nickname=world)

2.2 InheritableThreadLocal

代码语言:javascript
复制
class TransmittableThreadLocalTest5 {
    static ThreadLocal<UserSession> threadLocal = new InheritableThreadLocal<>();
    static ExecutorService executorService =
            Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开启");
        threadLocal.set(new UserSession("001","hello"));
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.get().setNickname("world");
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            //[读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.get().setNickname("Java");
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);
        //读取的是 Java(因为浅拷贝)
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    }
}
代码语言:javascript
复制
主线程开启
主线程读取本地变量:UserSession(uuid=001, nickname=hello)
子线程读取本地变量:UserSession(uuid=001, nickname=hello)
主线程读取本地变量:UserSession(uuid=001, nickname=world)
子线程读取本地变量:UserSession(uuid=001, nickname=world)
子线程读取本地变量:UserSession(uuid=001, nickname=Java)
主线程读取本地变量:UserSession(uuid=001, nickname=Java)

2.3 InheritableThreadLocal

代码语言:javascript
复制
class TransmittableThreadLocalTest6 {
    static ThreadLocal<UserSession> threadLocal = new TransmittableThreadLocal<>();
    static ExecutorService executorService =
            TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开启");
        threadLocal.set(new UserSession("001","hello"));
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);

        threadLocal.get().setNickname("world");
        System.out.println("主线程读取本地变量:" + threadLocal.get());

        executorService.submit(() -> {
            //[读到了主线程修改后的新值]
            System.out.println("子线程读取本地变量:" + threadLocal.get());
            threadLocal.get().setNickname("Java");
            System.out.println("子线程读取本地变量:" + threadLocal.get());
        });

        TimeUnit.SECONDS.sleep(1);
        //读取的是 Java(因为浅拷贝)
        System.out.println("主线程读取本地变量:" + threadLocal.get());
    }
}

输出结果与上面2.2的结果一样

参考文档:

  1. https://github.com/alibaba/transmittable-thread-local
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全菜工程师小辉 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ThreadLocal
    • ThreadLocal的使用场景
      • ThreadLocal的结构
        • ThreadLocal造成内存泄漏
          • ThreadLocal的最佳实践
          • InheritableThreadLocal
          • TransmittableThreadLocal
            • TransmittableThreadLocal实现原理
            • 补充说明
              • 1.线程局部变量为基础类型
                • 1.1 ThreadLocal
                • 1.2 InheritableThreadLocal
                • 1.3 TransmittableThreadLocal
              • 2.线程局部变量为类对象
                • 2.1 ThreadLocal
                • 2.2 InheritableThreadLocal
                • 2.3 InheritableThreadLocal
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档