写点什么

一文搞懂 ThreadLocal 原理

  • 2023-09-08
    福建
  • 本文字数:3722 字

    阅读完需:约 12 分钟

ThreadLocal 是什么


在多线程编程中,经常会遇到需要在不同线程中共享数据的情况。通常情况下,为了保证线程安全,我们需要使用锁或其他同步机制。然而,有些情况下,我们希望在每个线程中都有一份独立的数据副本,这就是 ThreadLocal 派上用场的地方。


ThreadLocal 翻译过来就是线程本地,也就是本地线程变量,意思是 ThreadLocal 中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal 提供了一种机制,允许我们为每个线程创建独立的变量,每个线程都可以独立访问自己的变量,而不会干扰其他线程的数据。ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,各个线程间互不影响,从而实现线程安全。

ThreadLocal 的原理


ThreadLocal 的原理涉及到两个重要概念:ThreadLocal 实例和 ThreadLocalMap。

1. ThreadLocal 实例

每个 ThreadLocal 对象实际上是一个容器,用于存储线程本地的变量副本。每个线程都可以拥有自己的 ThreadLocal 实例,这些实例可以存储不同的数据,互相之间互不影响。

2. ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的底层数据结构,它是一个哈希表。每个线程都有一个与之相关联的 ThreadLocalMap,用于存储该线程所拥有的 ThreadLocal 实例以及对应的值。ThreadLocalMap 中的键是 ThreadLocal 实例,值是该线程对应 ThreadLocal 实例的变量副本。

当我们调用 ThreadLocal 的 set()方法设置值时,实际上是在当前线程的 ThreadLocalMap 中以 ThreadLocal 实例为键,将值存储在对应的位置。而调用 get()方法时,则是从当前线程的 ThreadLocalMap 中根据 ThreadLocal 实例获取对应的值。

ThreadLocal 怎么用,应用场景


public static void main(String[] args) {   IntStream.range(1, 5).forEach(i -> new Thread(() -> {       // 设置线程中本地变量的值       threadLocal.set("thread-" + i);       // 打印当前线程中本地内存中本地变量的值       System.out.println(threadLocal.get());       // 清除本地内存中的本地变量       threadLocal.remove();       // 打印本地变量       System.out.println("thread-" + i + " after remove: " + threadLocal.get());   }).start());}/*thread-1thread-4thread-4 after remove: nullthread-2thread-3thread-2 after remove: nullthread-1 after remove: nullthread-3 after remove: null*/
复制代码

从结果可以看到,每一个线程都有各自的值,并且互不影响。

应用场景

  1. 用户访问之后,在本地线程保存用户的身份信息,在本次访问过程中,可以随时获取用户的身份信息以验证身份。

  2. 在进行对象跨层传递的时候,使用 ThreadLocal 可以避免多次传递,打破层次间的约束。

  3. 数据库连接管理:每个线程可以拥有自己的数据库连接,避免了线程之间的数据库连接混淆。

  4. 用户身份管理:在 Web 应用中,可以将用户身份信息存储在 ThreadLocal 中,以便在整个请求处理过程中方便地访问。

  5. 事务管理:将事务状态存储在 ThreadLocal 中,确保每个线程都能独立管理自己的事务状态。

ThreadLocal 源码分析

先看一下 ThreadLocal 和 Thread 的关系


图片


Thread 类中有一个 threadLocals 属性,是 ThreadLocal 内部类 ThreadLocalMap 类型的变量,ThreadLocalMap 可以看作是一个 HashMap,其内部有一个内部类为 Entry,继承了 WeakReference<ThreadLocal<?>>,是一个弱引用。Entry 的 key 是 ThreadLocal<?>,value 是 Object 类型的值。


大致了解了 Thread 和 ThreadLocal 的关系之后,看一下 Thread Local 的源码:我们只要看其主要的几个方法,就可以完全了解 ThreadLocal 的原理了。

set 方法


public void set(T value) {    // 获取当前线程    Thread t = Thread.currentThread();    // 通过当前线程获取线程中的ThreadLocal.ThreadLocalMap对象    ThreadLocalMap map = getMap(t);    if (map != null)        // map不为空,则直接赋值        map.set(this, value);    else        // map为空,则创建一个ThreadLocalMap对象        createMap(t, value);}// 根据提供的线程对象,和指定的值,创建一个ThreadLocalMap对象void createMap(Thread t, T firstValue) {    t.threadLocals = new ThreadLocalMap(this, firstValue);}// threadLocals是Thread类的一个属性ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}
/*Thread 类 182行 // ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. 与该线程有关的ThreadLocal值。这个映射由ThreadLocal类维护 ThreadLocal.ThreadLocalMap threadLocals = null;*/
复制代码

get 方法


// ThreadLocalMap中的内部类,存放key,valuestatic class Entry extends WeakReference<ThreadLocal<?>> {    // 与此ThreadLocal关联的值    Object value; // k:ThreadLocal的引用,被传递给WeakReference的构造方法    Entry(ThreadLocal<?> k, Object v) {        super(k);        value = v;    }}
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 通过当前线程获取线程中的ThreadLocal.ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) { // map不为空,通过this(当前对象,即ThreadLocal对象)获取Entry对象 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; // Entry不为空,则直接返回Entry中的value值 return result; } } // 如果map或Entry为空,则返回初始值-null return setInitialValue();}// 设置初始值,初始化ThreadLocalMap对象,并设置value为 nullprivate T setInitialValue() { // 初始化值,此方法返回 null T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value;}
复制代码

remove 方法


public void remove() {    // 通过当前线程获取线程中的ThreadLocal.ThreadLocalMap对象    ThreadLocalMap m = getMap(Thread.currentThread());    if (m != null)        // 移除对象        m.remove(this);}// 根据key,删除对应的所有值private void remove(ThreadLocal<?> key) {    Entry[] tab = table;    int len = tab.length;    // 获取key对应的 Entry[] 下标    int i = key.threadLocalHashCode & (len-1);    for (Entry e = tab[i];         e != null;         // 获取下一个Entry对象         e = tab[i = nextIndex(i, len)]) {        if (e.get() == key) {            e.clear();            // 通过重新哈希位于staleSlot和下一个null插槽之间的任何可能冲突的条目,来清除陈旧的条目。这还会清除尾随null之前遇到的所有其他过时的条目,防止出现内存泄漏问题            expungeStaleEntry(i);            return;        }    }}
复制代码


总结:

  1. 每个 Thread 维护着一个 ThreadLocalMap 的引用

  2. ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储

  3. ThreadLocal 创建的副本是存储在自己的 threadLocals 中的,也就是自己的 ThreadLocalMap。

  4. ThreadLocalMap 的键为 ThreadLocal 对象,而且可以有多个 threadLocal 变量,因此保存在 map 中

  5. 在进行 get 之前,必须先 set,否则会报空指针异常,当然也可以初始化一个,但是必须重写 initialValue()方法。

  6. ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

注意事项


虽然 ThreadLocal 在某些情况下非常有用,但过度使用它也可能导致内存泄漏问题。因为 ThreadLocalMap 中的数据只有在线程结束时才会被释放,如果没有正确地清理 ThreadLocal 实例,就可能会导致无限制的数据积累。


另外,ThreadLocal 不适合在并发量非常大的情况下使用,因为每个线程都会创建自己的变量副本,可能会导致内存消耗过大。

ThreadLocal 内存泄漏问题


在上面的代码中,我们可以看出,当前 ThreadLocal 的引用 k 被传递给 WeakReference 的构造函数,所以 ThreadLocalMap 中的 key 为 ThreadLocal 的弱引用。


如果当前线程一直存在且没有调用该 ThreadLocal 的 remove 方法,如果这个时候别的地方还有对 ThreadLocal 的引用,那么当前线程中的 ThreadLocalMap 中会存在对 ThreadLocal 变量的引用和 value 对象的引用,是不会释放的,会造成内存泄漏。


ThreadLocalMap 中的 Entry 的 key 使用的是 ThreadLocal 对象的弱引用,在没有其他地方对 ThreadLoca 依赖,ThreadLocalMap 中的 ThreadLocal 对象就会被回收掉,但是对应的 value 值不会被回收,这个时候 Map 中就可能存在 key 为 null 但是 value 不为 null 的项,也会造成内存泄漏。

小结


ThreadLocal 是一种多线程编程的工具,可以帮助我们在多线程环境中管理线程本地的变量。它通过 ThreadLocal 实例和 ThreadLocalMap 的组合实现了这一功能。


使用 ThreadLocal 时需要注意内存泄漏和性能问题,确保合理使用。


使用完 ThreadLocal 后,一定执行 remove 操作,避免出现内存泄漏情况。

发布于: 刚刚阅读数: 3
用户头像

IT领域从业者 分享见解 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
一文搞懂ThreadLocal原理_多线程_树上有只程序猿_InfoQ写作社区