Java多线程与高并发专题——在 Thread 中多个 ThreadLocal 是怎么存储的?

Java中Thread里多个ThreadLocal的存储解析

Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系

在解答本文的标题问题之前,先要搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系。

首先我们梳理下它们的定义与作用:

Thread(线程)

  • 定义:Thread 是 Java 中的线程类,它代表了一个单独的执行单元。每个线程都有自己的执行栈和局部变量。

  • 作用:Thread 用于创建和管理线程,使程序能够并发执行。

ThreadLocal(线程本地变量)

  • 定义:ThreadLocal 是一个用于在多线程环境中为每个线程提供独立变量副本的类。每个线程都可以独立地设置和获取自己的变量副本,而不会与其他线程的变量副本发生冲突。

  • 作用:ThreadLocal 用于实现线程安全的变量访问,避免了使用同步机制的开销。

ThreadLocalMap(线程本地映射)

  • 定义:ThreadLocalMap 是 ThreadLocal 的一个内部静态类,用于存储每个线程的线程本地变量的键值对。它是一个哈希表,键是 ThreadLocal 对象,值是线程本地变量的值。

  • 作用:ThreadLocalMap 用于在每个线程中维护线程本地变量的映射关系,使得每个线程可以独立地访问自己的变量副本。

三者之间的关系

下面我们用最直观、最容易理解的图画的方式来看看它们三者的关系:

我们看到最左下角的 Thread 1,这是一个线程,它的箭头指向了  ThreadLocalMap 1,其要表达的意思是,每个 Thread 对象中都持有一个 ThreadLocalMap 类型的成员变量,在这里 Thread 1 所拥有的成员变量就是 ThreadLocalMap 1。

而这个 ThreadLocalMap 自身类似于是一个 Map,里面会有一个个 key value 形式的键值对。那么我们就来看一下它的 key 和 value 分别是什么。可以看到这个表格的左侧是 ThreadLocal 1、ThreadLocal 2…… ThreadLocal n,能看出这里的 key 就是 ThreadLocal 的引用。

而在表格的右侧是一个一个的 value,这就是我们希望 ThreadLocal 存储的内容,例如 user 对象等。

这里需要重点看到它们的数量对应关系:一个 Thread 里面只有一个ThreadLocalMap ,而在一个ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map的数据结构来存放 ThreadLocal 和 value。

通过这张图片,我们就可以搞清楚 Thread、 ThreadLocal 及 ThreadLocalMap 三者在宏观上的关系了。

Thread >ThreadLocalMap>ThreadLocal

  • 每个 Thread 对象内部都有一个 ThreadLocalMap 类型的成员变量,用于存储该线程的线程本地变量映射。

  • ThreadLocalMap 是 ThreadLocal 的载体,每个线程通过 ThreadLocalMap 来管理自己的线程本地变量。

ThreadLocal 操作 ThreadLocalMap

  • ThreadLocal 提供了 get、set、remove 等方法,这些方法实际上是对 ThreadLocalMap 进行操作。

  • 当一个线程调用 ThreadLocal.set(value) 方法时,实际上是在该线程的 ThreadLocalMap 中存储一个键值对,键是当前的 ThreadLocal 对象,值是传入的 value。

  • 当一个线程调用 ThreadLocal.get() 方法时,实际上是从该线程的 ThreadLocalMap 中获取与当前 ThreadLocal 对象关联的值。

ThreadLocalMap 的生命周期

  • ThreadLocalMap 的生命周期与 Thread 对象的生命周期绑定。当一个线程结束时,其对应的 ThreadLocalMap 也会被销毁。

  • ThreadLocalMap 是线程私有的,每个线程都有自己的 ThreadLocalMap,不会与其他线程共享。

源码分析

知道了它们的关系之后,我们再来进行源码分析,来进一步地看到它们内部的实现。

我们先从ThreadLocal 开始,对于ThreadLocal我们通过前面的ThreadLocal 是用来解决共享资源的多线程访问的问题吗?ThreadLocal 适合用在哪些实际生产的场景中?已经有了不错的了解,我们下面从它的一些具体方法去看看。

ThreadLocal的get 方法

我们来看一下 get 方法,源码注释如下:

Returns the value in the current thread's copy of this thread-local variable. If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the initialValue method.

翻译:

返回此线程局部变量在当前线程副本中的值。如果该变量对于当前线程没有值,那么它首先会被初始化为调用 `initialValue` 方法所返回的值。 

源码代码如下:

    /**
     * 获取当前线程中此线程局部变量副本的值。
     * 如果当前线程的该变量还没有值,会首先调用 {@link #initialValue} 方法对其进行初始化。
     *
     * @return 当前线程中此线程局部变量的值
     */
    public T get() {
        // 获取当前正在执行的线程
        Thread t = Thread.currentThread();
        // 从当前线程中获取对应的 ThreadLocalMap,该 map 存储了线程局部变量
        ThreadLocalMap map = getMap(t);
        // 检查当前线程的 ThreadLocalMap 是否存在
        if (map != null) {
            // 从 ThreadLocalMap 中获取与当前 ThreadLocal 对象关联的条目
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 检查获取的条目是否存在
            if (e != null) {
                // 抑制编译器对类型转换的警告,因为这里可以确定值的类型
                @SuppressWarnings("unchecked")
                // 从条目中提取值并转换为泛型类型 T
                T result = (T)e.value;
                // 返回获取到的值
                return result;
            }
        }
        // 如果 ThreadLocalMap 不存在或者条目不存在,调用 setInitialValue 方法设置初始值并返回
        return setInitialValue();
    }

这是 ThreadLocal 的 get 方法,可以看出它利用了 Thread.currentThread 来获取当前线程的引用,并且把这个引用传入到了 getMap 方法里面,来拿到当前线程的 ThreadLocalMap。

然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。

值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。

ThreadLocal的getMap 方法

下面我们来看一下 getMap 方法,源码如下所示:

    /**
     * 获取与指定线程关联的 ThreadLocalMap。
     * 该方法用于获取指定线程的 ThreadLocalMap 实例,
     * 该实例存储了线程局部变量的映射关系。
     *
     * @param t 当前线程
     * @return 与指定线程关联的 ThreadLocalMap,如果线程没有关联的 ThreadLocalMap,则返回 null。
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到,这个方法很清楚地表明了 Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap是线程的一个成员变量。这个方法的作用就是获取到当前线程内的 ThreadLocalMap 对象,每个线程都有 ThreadLocalMap 对象,而这个对象的名字就叫作 threadLocals,初始值为 null,代码如下:

    /**
     * 与当前线程相关的ThreadLocal值。这个映射由ThreadLocal类维护。
     * 每个线程都有自己独立的ThreadLocalMap实例,用于存储该线程的ThreadLocal变量的值。
     * 当线程使用ThreadLocal对象存储值时,这些值实际上是存储在该线程的ThreadLocalMap中。
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal的set 方法

下面我们再来看一下 set 方法,源码注释如下:

Sets the current thread's copy of this thread-local variable to the specified value. Most subclasses will have no need to override this method, relying solely on the initialValue method to set the values of thread-locals.

翻译:

将此线程局部变量的当前线程副本设置为指定值。大多数子类无需重写此方法,仅依靠 initialValue 方法来设置线程局部变量的值即可。

 源码代码如下:

    /**
     * 设置当前线程的此线程局部变量的副本为指定的值。
     * 大多数子类无需重写此方法,仅依赖 {@link #initialValue} 方法来设置线程局部变量的值。
     *
     * @param value 要存储在此线程局部变量的当前线程副本中的值。
     */
    public void set(T value) {
        // 获取当前正在执行的线程
        Thread t = Thread.currentThread();
        // 获取当前线程关联的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 若该线程的 ThreadLocalMap 已存在
        if (map != null)
            // 将当前 ThreadLocal 对象作为键,指定的值作为值,存入 ThreadLocalMap
            map.set(this, value);
        else
            // 若 ThreadLocalMap 不存在,则为当前线程创建一个新的 ThreadLocalMap,并将初始值存入
            createMap(t, value);
    }

set 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。

可以看出,map.set(this, value)  传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。

ThreadLocalMap 类

下面我们来看一下 ThreadLocalMap 这个类,其源码注释如下:

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. No operations are exported outside of the ThreadLocal class. The class is package private to allow declaration of fields in class Thread. To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.

翻译:

ThreadLocalMap是一种定制的哈希映射,仅适用于维护线程局部值。没有任何操作会导出到ThreadLocal类之外。该类是包私有类,目的是允许在Thread类中声明字段。为了便于处理非常大且长期存在的使用场景,哈希表条目对键使用弱引用。然而,由于未使用引用队列,只有当表开始空间不足时,过期条目才一定会被移除。

通过前面我们知道,它就是 Thread.threadLocals,在ThreadLocalMap中的Entry类的源码注释如下:

The entries in this hash map extend WeakReference, using its main ref field as the key (which is always a ThreadLocal object). Note that null keys (i. e. entry. get() == null) mean that the key is no longer referenced, so the entry can be expunged from table. Such entries are referred to as "stale entries" in the code that follows.

翻译:

此哈希映射中的条目扩展自WeakReference,将其main ref字段用作键(该键始终是一个ThreadLocal对象)。请注意,空键(即entry.get() == null)意味着该键不再被引用,因此该条目可以从表中清除。在后续代码中,此类条目被称为“过期条目”。

下面我们看看它的实现源码:

        /**
         * 该类继承自 WeakReference,用于表示 ThreadLocalMap 中的条目。
         * 每个条目包含一个对 ThreadLocal 对象的弱引用和与之关联的值。
         * 弱引用的使用允许在没有其他强引用指向 ThreadLocal 对象时,该对象可以被垃圾回收。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 与该 ThreadLocal 关联的值。 */
            Object value;

            /**
             * 构造一个新的 Entry 对象。
             *
             * @param k 与该条目关联的 ThreadLocal 对象。
             * @param v 与该 ThreadLocal 对象关联的值。
             */
            Entry(ThreadLocal<?> k, Object v) {
                // 调用父类 WeakReference 的构造函数,传入 ThreadLocal 对象
                super(k);
                // 初始化与该 ThreadLocal 关联的值
                value = v;
            }
        }
        /**
         * 存储线程本地变量条目的数组,根据需要进行调整大小。
         * 数组的长度必须始终是 2 的幂次方,以确保哈希计算的一致性。
         */
        private Entry[] table;

ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,其中最重要的就是截取出的这段代码中的 Entry 内部类。在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把Entry 理解为一个 map,其键值对为:

  • 键,当前的 ThreadLocal;
  • 值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。

ThreadLocalMap 既然类似于 Map,所以就和 HashMap 一样,也会有包括 set、get、rehash、resize 等一系列标准操作。

但是,虽然思路和 HashMap 是类似的,但是具体实现会有一些不同。

比如其中最重要的一个不同点就是 ThreadLocalMap 和 HashMap 在处理冲突时的区别。我们在为什么 Map 桶中超过 8 个才转为红黑树?有提到 HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链。但是 ThreadLocalMap 解决 hash 冲突的方式是不一样的,它采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。

### Java多线程高并发实战项目示例教程 #### 推荐开源项目 为了更好地理解和掌握Java中的并发编程技术,在开发高性能、响应迅速的应用程序时,可以参考名为“Java Concurrency/Multithreading Examples”的开源项目[^1]。此项目不仅提供详尽的教程还附带大量实用的代码实例。 #### 开发指南 当涉及到具体实现方面,创建新线程可以通过继承`Thread`类或是实现`Runnable`接口这两种基本方法来完成;对于定时任务,则有诸如`Timer`这样的工具可供选用[^2]。针对多个线程间共享数据的需求,除了直接操作公共变量外,还可以借助于`ThreadLocal`这种特殊类型的容器以确保每个访问它的线程都有自己独立的一份副本。而在处理更复杂的业务逻辑时,像原子性类(`AtomicInteger`, `AtomicLong`)能保证单个数值更新的安全性,而高级别的同步辅助类如`Semaphore`(信号量), `CyclicBarrier`, 和`CountDownLatch`则可用于协调不同工作单元之间的执行顺序和依赖关系。 #### 并发控制手段 面对高负载环境下的资源竞争状况,合理的配置线程池参数以及选择合适的锁机制变得尤为重要。例如,在电商秒杀活动中,适当调整核心/最大池大小、存活时间等属性能够有效提升系统的吞吐率和服务质量;此同时,采用读写分离模式(即ReadWriteLock)可进一步提高数据库查询效率减少锁定冲突的发生概率[^3]。 #### 经典应用场景分析 考虑到实际工作中可能遇到的具体挑战之一——哲学家就餐问题,这是一个经典的死锁预防案例研究。通过引入条件变量Condition对象配合ReentrantLock重入锁一起使用,可以在不破坏原有算法结构的前提下成功打破循环等待链从而避免发生永久性的阻塞现象[^5]。 ```java // 使用 ReentrantLock 解决哲学家就餐问题的一个片段 final int NUM_PHILOSOPHERS = 5; final Lock[] forks = new ReentrantLock[NUM_PHILOSOPHERS]; for (int i = 0; i < NUM_PHILOSOPHERS; ++i) { forks[i] = new ReentrantLock(true); } class Philosopher implements Runnable { private final int id; public void run() { while (true) { think(); synchronized(this){ try{ wait(); //模拟思考过程 }catch(InterruptedException e){} } pickUpForks(id); //尝试获取筷子 eat(); putDownForks(id); //放下筷子 } } private void pickUpForks(int philosopherId) throws InterruptedException { if ((philosopherId & 1) == 0) { //偶数号先拿左边再右边 forks[philosopherId].lockInterruptibly(); forks[(philosopherId + 1) % NUM_PHILOSOPHERS].lockInterruptibly(); } else { //奇数号相反 forks[(philosopherId + 1) % NUM_PHILOSOPHERS].lockInterruptibly(); forks[philosopherId].lockInterruptibly(); } } ... } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黄雪超

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值