一文理解ThreadLocal

本文讲解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方法:

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:

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相关逻辑如下:

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

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());
    }
}

输出结果为:

主线程开启
主线程读取本地变量:1
子线程读取本地变量:null
主线程读取本地变量:2
子线程读取本地变量:null
子线程读取本地变量:3
主线程读取本地变量:2

1.2 InheritableThreadLocal

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());
    }
}

输出结果为:

主线程开启
主线程读取本地变量:1
子线程读取本地变量:1
主线程读取本地变量:2
子线程读取本地变量:1
子线程读取本地变量:3
主线程读取本地变量:2

1.3 TransmittableThreadLocal

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());
    }
}

输出结果为:

主线程开启
主线程读取本地变量:1
子线程读取本地变量:1
主线程读取本地变量:2
子线程读取本地变量:2
子线程读取本地变量:3
主线程读取本地变量:2

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

首先定义一个数据类:

@Data
@AllArgsConstructor
class UserSession{
    String uuid;
    String nickname;
}

2.1 ThreadLocal

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());
    }
}

输出结果为:

主线程开启
主线程读取本地变量:UserSession(uuid=001, nickname=hello)
子线程读取本地变量:null
主线程读取本地变量:UserSession(uuid=001, nickname=world)
子线程读取本地变量:null
主线程读取本地变量:UserSession(uuid=001, nickname=world)

2.2 InheritableThreadLocal

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());
    }
}
主线程开启
主线程读取本地变量: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

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

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值