欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

ThreadLocal的应用及原理,这些变量与普通变量的

来源: javaer 分享于  点击 15793 次 点评:235

ThreadLocal的应用及原理,这些变量与普通变量的


1. ThreadLocal 是什么

JDK 对ThreadLocal的描述为:

此类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量的线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。

说白了,ThreadLocal就是用来存放线程自身相关数据的一个容器,这个容器叫做ThreadLocalMap,它是ThreadLocal的一个静态内部类,同时作为Thread类的一个成员变量。ThreadLocal在使用时,先拿到当前线程的成员变量ThreadLocalMap,以当前的ThreadLocal对象作为key,变量作为value 存入ThreadLocalMap。 然后每个线程取变量都是从线程各自的ThreadLocalMap中取值,自然是线程安全的了。因为变量只在自己线程的生命周期内起作用,所以说ThreadLocal 提供线程局部变量,或者叫线程本地变量。

ThreadLocal 的特点有3个:

  1. 线程并发:在多线程并发的场景下使用。
  2. 数据传递:通过 ThreadLocal ,在同一个线程中,不同组件中传递公共变量。
  3. 线程隔离:不同线程之间互不干扰,这种变量在线程的生命周期内起作用。

2. ThreadLocal 怎么用

ThreadLocal 的常用方法有:

  1. public ThreadLocal():通过构造器创建对象。一般是静态的。
  2. <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier):初始化一个 ThreadLcoal。
  3. void set(T value):设置当前线程绑定的局部变量。
  4. T get():获取当前线程绑定的局部变量。
  5. void remove():删除当前线程绑定的局部变量。

2.1 使用入门

2.1.1 原始版本

现在模拟一个需求,一个线程在业务开始时初始化一个用户 id(类似在一次web请求中上下文中初始化一下用户信息),业务结束时获取这个用户 id(比如用来打印日志,或者作为一个公共变量运用到业务编码中),存在多个这样的线程。

public class ThreadLocalTest {
    private String userId;

    private String getUserId() {
        return userId;
    }

    private void setUserId(String userId) {
        this.userId = userId;
    }

    public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();
        for (int i = 1; i < 6; i++) {
            Thread thread = new Thread(() -> {
                // 当前线程初始化userId
                test.setUserId(Thread.currentThread().getName() + "的userId");
                // 执行其他业务代码
                System.out.println("===执行业务代码===");
                // 当前线程获取userId
                System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

一种可能的结果:

===执行业务代码===
线程2-->线程1的userId
===执行业务代码===
线程1-->线程3的userId
===执行业务代码===
线程3-->线程3的userId
===执行业务代码===
线程4-->线程4的userId

由于线程调度的不确定性,可能线程1运行到一半,切换到了线程2,于是线程2获取到的 userId 是线程1设置的。也就是说,每个线程之间的变量不是隔离的,造成数据错误。

2.1.2 ThreadLocal 版本

每个线程中的变量都存放到自己的线程当中,所以这些变量叫做线程局部变量很形象。

public class ThreadLocalTest {
    private static ThreadLocal<String> context = new ThreadLocal<>();

    private String getUserId() {
        return context.get();
    }

    private void setUserId(String userId) {
        context.set(userId);
    }

    public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();
        for (int i = 1; i < 5; i++) {
            Thread thread = new Thread(() -> {
                test.setUserId(Thread.currentThread().getName() + "的userId");
                System.out.println("===执行业务代码===");
                System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
                context.remove(); // 使用完清理线程局部变量
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

这样每个线程就互不干扰,不会取错变量值。一种可能的结果如下:

===执行业务代码===
线程1-->线程1的userId
===执行业务代码===
线程4-->线程4的userId
===执行业务代码===
线程2-->线程2的userId
===执行业务代码===
线程3-->线程3的userId

2.1.3 synchronized 版本

如果只看结果的正确性,用 synchronized 给业务代码块加锁也是可以完成的。如下:

Thread thread = new Thread(() -> {
    synchronized (ThreadLocalTest.class) {
        test.setUserId(Thread.currentThread().getName() + "的userId");
        System.out.println("===执行业务代码===");
        System.out.println(Thread.currentThread().getName() + "->" + test.getUserId());
    }
});

这样完全可以实现需求,但是 synchronized 的问题是什么呢?我们总说谁谁谁是线程安全的类,因为它有 synchronized 修饰。就是因为 synchronized 让多线程变成了单线程,它一次只允许一个线程执行,它能不安全吗?但它带来的代价是性能的下降,它不能并发执行,而 ThreadLocal 可以并发执行。

2.1.4 ThreadLocal 和 synchronized 对比

综上,synchronized 和 ThreadLocal 两个处理问题的角度和场景是不同的。

  • synchronized 的侧重点在于保证操作的原子性,保证并发场景下共享变量的数据一致性。
  • ThreadLocal 强调线程隔离性,不同的线程互不干扰,保证并发场景下数据传递的正确性。在web请求上下文中较为常见。

3. ThreadLocal 原理

3.1 代码结构

ThreadLocal 的原理要从它的set(T value)get()方法的源码入手。在 set 值的时候,首先会获取当前线程一个的成员变量ThreadLocalMapThreadLocalMap的 key 是当前ThreadLocal对象,value 是要存入的值。这个 key 和 value 会存到哪里呢?ThreadLocalMap还有个内部类Entry,这个Entry继承了WeakReference,key 赋值给弱引用,也就是当前的ThreadLocal对象,value 则赋值给Entry的成员变量valueThreadLocalMap也是一个哈希表(所谓哈希表,也叫散列表,它基于数组,通过某种哈希算法计算出一系列关键字对应的散列值,然后以这些散列值作为数组索引将数据存放到对应位置,达到快速查找的目的),它内部维护一个Entry数组,来存储键值对。存数据的时候也是通过哈希函数计算ThreadLocal 对象对应的数组下标,然后放入Entry数组中。

3.2 内存泄漏问题

ThreadLocal 会发生内存泄漏吗?我们结合代码慢慢分析。

在 2.1.1 节中有这样的代码:

public class ThreadLocalTest {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private void setUserId(String userId) {
        threadLocal.set(userId);
    }
    // ...
}

首先,我们new了一个 ThreadLocal 对象,这里存在一个强引用:threadLocal引用变量指向 ThreadLocal 对象。其次,当其他线程执行setUserId方法时,ThreadLocal 的set方法最终是把数据存到了ThreadLocalMap中的Entry,看源码我们会发现,存数据最终是调用Entry的构造器Entry(ThreadLocal<?> k, Object v)完成的,而k这个参数是传入的this对象,说明什么?我们使用 ThreadLocal 对象调用set,那this肯定是当前new出来的 ThreadLocal 对象!再次说明,我们new出来的 ThreadLocal 对象有两个引用指向它:

  1. threadLocal变量的强引用。
  2. Entry中的弱引用。

此时再看一张图(这张图被广泛引用,感谢原图作者

相关栏目:

用户点评