ThreadLocal的使用和源码

目录

1、什么是ThreadLocal?有什么用?

2、ThreadLocal方法、概念详解

3、ThreadLocal的常见问题:

4、多线程下如何实现上下文的传递


1、什么是ThreadLocal?有什么用?

ThreadLocal 是 Java 并发模型中的一个关键工具类,用于实现线程隔离(Thread Confinement) ,即:为每个线程提供独立的变量副本,避免多线程之间的共享与竞争。

简单理解:ThreadLocal可以实现线程之间的变量互相隔离,也就可以理解为线程的私有数据。

例如:

我们常用的将用户ID存储到ThreadLocal中就可以实现当前线程中可以一直使用。

多数据源时将当前线程使用的数据源存在ThreadLocal中后续使用。

2、ThreadLocal方法、概念详解

void set(T value)方法详解

当我们调用set方法时,可以看到首先获取到当前线程,然后调用getMap方法,这个getMap方法返回了一个ThreadLocalMap方法,然后如果map不为空,将当前ThreadLocal对象和值转为Entry对象放入到这个map中。

tab[i] = new Entry(key, value);

由此我们可以看出当我们调用set方法时,是将当前ThreadLocal对象作为key,value作为值构建了一个Entry对象,然后存入到ThreadLocalMap对象中。

java 复制代码
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
// 这个是上面的map.set方法
private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 将当前示例对象ThreadLocal、value作为Entry对象存入
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }


ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocalMap是Thread类中的的一个成员变量

void remove()方法详解

remove方法就很简单了,就是获取当前线程的ThreadLocalMap对象,然后清理。

java 复制代码
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

T get()方法详解

获取ThreadLocalMap,将当前ThreadLocal对象作为Key存储,如果值不为空就返回,为空则返回null

java 复制代码
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

Thread、ThreadLocal、ThreadLocalMap的关系

看了上面可能会疑惑Thread、ThreadLocal、ThreadLocalMap都有什么关系呢?

ThreadLocalMap是Thread的一个成员变量,ThreadLocal是作为key存储在ThreadLocalMap中的

Thread

└── ThreadLocalMap(每个线程独有)

├── Entry(ThreadLocal1 → value1)

├── Entry(ThreadLocal2 → value2)

├── Entry(ThreadLocal3 → value3)

Entry是什么

结论:entry对象是存储在ThreadLocalMap中的基本对象。ThreadLocalMap中寸的就是一个个的entry对象。从ThreadLocal的set方法中可以看出:

就是将当前ThreadLocal对象作为key,要存的值作为value构建Entry对象放入ThreadLocalMap中

java 复制代码
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
// 这个是上面的map.set方法
private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 将当前示例对象ThreadLocal、value作为Entry对象存入
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

根据代码可以看出Entry对象的key是弱引用,value是正常的也就是强引用,这也是后面会发生内存泄漏的关键。

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

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

3、ThreadLocal的常见问题:

3.1什么是内存泄漏

标准回答:程序中某些对象已经"不再被使用",但仍然被引用,导致 GC 无法回收,从而长期占用内存。

大白话:对象创建了,但是使用完没销毁,导致一直存放在内存中,白白占用空间,还回收不了。

3.2为什么必须调用 remove?不remove为啥会内存泄漏?

首先我们要明白ThreadLocal是如何存放数据的。

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

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

存入的key是弱引用,value是强引用,如果在正常请求中我们请求结束也就没有引用了,就可以被正常的gc回收掉。

但是如果在线程池中,线程是复用的,如果没有调用remove方法,就会导致这个对象不再被使用。但是一直存在,而且还是强引用,被回收不掉,就发生了内存泄漏。

GC Roots

Thread(线程池线程不会销毁)

ThreadLocalMap

Entry

value

例如:

我在使用线程池的时候,每次都new一个ThreadLocal对象,然后存当前用户的信息

当我第一次调用时

ThreadLocal.set(new ThreadLocal(),userInfo1)

第二次调用时

ThreadLocal.set(new ThreadLocal(),userInfo2)

频繁下来,userInfo就一直存在内存中没有清楚,而且还可能导致数据错乱

3.3为什么 ThreadLocalMap 的 key 要设计为弱引用?如果是强引用会有什么问题?

ThreadLocalMap 的 key 设计为弱引用,是为了避免 ThreadLocal 对象无法被回收,从而导致更严重的内存泄漏

弱引用能保证GC可以回收ThreadLocal对象

  • 在线程池中,线程对象不会被销毁
  • 如果 key 是强引用 → ThreadLocal 永远被 Thread 持有 → value 也无法回收 → 内存泄漏严重

3.4异步场景为什么会丢上下文?

异步场景会丢失上下文,是因为 ThreadLocal 的数据存储在"当前线程"中,而异步任务通常运行在"另一个线程"上

Thread A

└── ThreadLocalMap

└── user = A

Thread B

└── ThreadLocalMap

└── user = null

线程之间没有共享

例如:

java 复制代码
ThreadLocal<String> tl = new ThreadLocal<>();

// 主线程
tl.set("userA");

executor.submit(() -> {
    System.out.println(tl.get()); // 拿不到
});

主线程(Thread A)

↓ set(userA)

线程池线程(Thread B)

↓ 执行任务

↓ get() → 取自己的 ThreadLocalMap → 没有数据

4、多线程下如何实现上下文的传递

TransmittableThreadLocal

TTL 可以在"线程切换(尤其线程池复用)"时,把 ThreadLocal 的值从提交线程传递到执行线程

  1. 捕获上下文(提交任务时)

主线程:

ThreadLocalMap → 拷贝一份


  1. 设置上下文(执行任务时)

执行线程:

把拷贝的数据 set 到当前线程


  1. 恢复现场(执行完成)

恢复线程原有的 ThreadLocal 数据

👉 防止污染线程池线程

相关推荐
SarL EMEN2 小时前
Spring boot创建时常用的依赖
java·spring boot·后端
随风,奔跑2 小时前
Spring Data Redis
java·redis·spring
csbysj20202 小时前
JSP 语法详解
开发语言
roamingcode2 小时前
应对 Codex 0.118.0 破坏性更新:Slash Prompt Router 架构解析与实践
java·开发语言·prompt·codex·skill
计算机学姐2 小时前
基于SpringBoot的特色美食分享系统
java·vue.js·spring boot·后端·spring·tomcat·mybatis
zzginfo2 小时前
JavaScript 假值示例详解
开发语言·前端·javascript·ecmascript
421!2 小时前
C语言学习笔记——10(结构体)
c语言·开发语言·笔记·stm32·学习·算法
551只玄猫2 小时前
【数学建模 matlab 实验报告5】最短路问题作业
开发语言·数学建模·matlab·课程设计·图论·最短路径·实验报告
不只会拍照的程序猿2 小时前
《嵌入式AI筑基笔记04:python函数与模块01—从C的刻板到Python的灵动》
c语言·开发语言·笔记·python