Java ThreadLocal

ThreadLocal 用于 线程间数据隔离 , 每个线程都有变量的副本, 每个线程同一时间访问的并不是同一个对象.

ThreadLocal的使用场景

引用一下 When to use ThreadLocal? 这个篇教程的说法:

Consider you are working on a eCommerce application.

You have a requirement to generate a unique transaction id for each and every customer request this controller process and you need to pass this transaction id to the business methods in manager/DAO classes for logging purpose.

One solution could be passing this transaction id as a parameter to all the business methods. But this is not a good solution as the code is redundant and unnecessary.

To solve that, here you can use ThreadLocal variable.

You can generate a transaction id in controller OR any pre-processor interceptor; and set this transaction id in the ThreadLocal.

After this, whatever the methods, that this controller calls, they all can access this transaction id from the threadlocal.

Also note that application controller will be servicing more that one request at a time and since each request is processed in separate thread at framework level, the transaction id will be unique to each thread and will be accessible from all over the thread’s execution path.

简而言之, ThreadLocal 用于把一个 T value 变量绑定到线程上, 每个线程都有这个变量的不同实例, 任何线程在执行程序时, 都可以访问自己独立的 ThreadLocal 变量.

有一种替代 ThreadLocal 变量的方法是, 把 set 到 ThreadLocal 里的 value 当做方法参数传递到任何需要用到它的地方. 不过这种方法在一些情况下是重复繁琐的, 比如:

pic1

许多用户访问 ResourceA, 请求中有一些用户信息(如, userId, deviceId, AppVersion等), ResourceA 对请求的处理逻辑比较复杂, 会嵌套调用很多别的类的方法, 这些方法有的会用到 userId, 有的会用到 deviceId 等等. 这种情况下, 层层传递用户信息就不太合适, 这需要把信息从 ResourceA 一直传递到 最后一层函数的参数中(中间许多方法可能并不需要使用用户信息).

我们知道, 不同的 API 请求到达时, Java框架 都会用线程池里的线程处理, 一个API请求由一个线程(及其子线程)执行到结束(API响应返回).

在这场合, 我们完全可以通过 ThreadLocal 把不同用户信息 set 到不同的线程上, 在线程需要使用 用户信息时, 再直接 get 拿到用户信息, 这种方法更为优雅.

ThreadLocal 使用示例

ThreadLocal主要有以下几个方法:

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } // 设置当前线程中 ThreadLocal变量的副本
public void remove() { } //移除当前线程中 ThreadLocal变量的副本

下面的示例代码中, 有两个 ThreadLocal 常量 LOCAL_USER 和 TEST, 这两个常量的 hashcode 是不同的. hashcode 只由线程所拥有的 ThreadLocal 数量决定.

主要看一下 LOCAL_USER 常量:

public class ThreadLocalTest {
    static final ThreadLocal<User> LOCAL_USER = new ThreadLocal<>();
    static final ThreadLocal<Integer> TEST = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        User user1 = new User("123","Mitre",13);
        LOCAL_USER.set(user1);
        System.out.println("main: " + LOCAL_USER.get());

        TEST.set(123);
        System.out.println("main: " + TEST.get());

        System.out.println("---t1 线程---");
        Thread t1 = new Thread(() -> {
            //
            System.out.println("t1, before set: " + LOCAL_USER.get());
            // new User
            User user = new User("666","Rosie",647);
            LOCAL_USER.set(user);
            System.out.println("t1, after set: " + LOCAL_USER.get());
            LOCAL_USER.remove();
            System.out.println("t1, after remove: " + LOCAL_USER.get());
        });
        t1.start();
        t1.join();

        System.out.println("\nmain: " + LOCAL_USER.get());
        System.out.println("---t2 线程---");
        // 不良示范, ThreadLocal 用于线程隔离, 不应该把共享变量传给ThreadLocal
        Thread t2 = new Thread(() -> {
            System.out.println("t2, before set: " + LOCAL_USER.get());
            // 把共享变量传给ThreadLocal, ThreadLocal set 时, 会修改共享变量的值
            LOCAL_USER.set(user1);
            // user1.name 会由 "Mitre" -> "A BAD TRYING"
            LOCAL_USER.get().setName("A BAD TRYING.");
            System.out.println("t2, after set: " + LOCAL_USER.get());
            LOCAL_USER.remove();
            System.out.println("t2, after remove: " + LOCAL_USER.get());
        });
        t2.start();
        t2.join();

        System.out.println("\nmain: " + LOCAL_USER.get());
    }
}

结果:

main: User{id='123', name='Mitre', age=13}
main: 123
---t1 线程---
t1, before set: null
t1, after set: User{id='666', name='Rosie', age=647}
t1, after remove: null

main: User{id='123', name='Mitre', age=13}
---t2 线程---
t2, before set: null
t2, after set: User{id='123', name='A BAD TRYING.', age=13}
t2, after remove: null

main: User{id='123', name='A BAD TRYING.', age=13}

分析:

LOCAL_USER 在main线程中设置了值, 在 main 线程中可以正常获取 LOCAL_USER 的变量副本, 而在子线程(t1 和 t2) 中是获取不到 main 线程的 LOCAL_USER 的变量副本的. (如果想让子线程获取到父线程的 ThreadLocal 变量, 可以使用 InheritableThreadLocal 变量代替 ThreadLocal)

t1 线程是常规的使用 ThreadLocal 的方式, 我们应该像t1线程这样使用: 先 set 一个变量副本到 ThreadLocal, 然后在需要用到的地方 get, 在线程退出之前 remove.

t2 线程不应该用一个共享变量作为ThreadLocal的变量副本, 这样在 t2 线程中修改变量副本的值, 会影响共享变量, 因为变量实例只有一个, ThreadLocal 变量副本使用共享变量时用的是引用, 而不是 deep copy 了一个变量. 从程序输出的最后一行可以看出, t2 线程对 共享变量 user1 的修改生效了.

ThreadLocal的实现原理

所谓实现原理, 看 源码 和 doc说明 或许是最为直接的.

这里给出几个结论:

1, ThreadLocal 变量并不持有变量副本value, ThreadLocal 主要有两个作用: 一是提供 hashcode 算法(魔数); 二是作为 ThreadLocalMap.Entry 的 key.

2, ThreadLocalMap.Entry 持有 value, 同时持有 key(即ThreadLocal变量)的弱引用.

3, 把 key 设计成弱引用是为了防止内存泄漏, 但通常 ThreadLocal 变量都会有其他的强引用(我们通常会把ThreadLocal定义成常量, 所以总会有ThreadLcoal总会有强引用, 这种情况下弱引用就形同虚设了), 所以最好手动 remove 不用的 ThreadLocal变量, 参考这里.

4, ThreadLocal 无法让子线程可以访问到父线程的 value, 需要子线程访问父线程的value 可以使用 InheritableThreadLocal.

参考

[1] 死磕Java源码之ThreadLocal实现分析

[2] 超强解析:ThreadLocal的使用与原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值