Skip to content

ThreadLocal

约 7107 字大约 24 分钟

2025-03-12

介绍一下 ThreadLocal

Thread类有两个变量:threadLocals和inheritableThreadLocals

这两个变量默认为null,只有当该线程调用了ThreadLocal类的get/set方法时才会创建他们,而调用ThreadLocal的get/set实际上是调用ThreadLocalMap的get/set

ThreadLocalMap可理解成给ThreadLocal定制化的HashMap

最终的变量放在了线程的ThreadLocalMap中,而不是ThreadLocal中,ThreadLocal只是对其进行封装,向其传递变量值

假设一个请求中,A方法调->B方法->调C方法->调D方法的情况。

假设D方法需要使用在入口处拿到的userid参数,那没有ThreadLocal时,我们需要把userid一步一步通过方法参数的方式,传递给D; 而用threadlocal的方式,在A方法存进去,D方法直接从threadlocal取就行,不用通过中间方法的传递。

所以ThreadLocal一般用来存储userId,traceId等在整个线程内随时可能存取的内容

ThreadLocal-1.png

ThreadLocal 内存泄露问题了解吗

ThreadLocal 内存泄露也是个老生常谈的问题了,网上部分资料,包括好多面试官都把这个问题出现的主要原因归结为 ThreadLocalMaps 里 Entry Key(ThreadLocal 对象本身)使用了弱引用导致的,但是我们仔细看看引用结构,脑补一下内存泄露的场景就不难发现,真正导致内存泄露的主要原因,其实是 Thread 强引用 ThreadLocalMaps,如果 Thread 一直存在,ThreadLocalMaps Entry 中的 value 这个强引用一直存在,不被回收才是导致发生内存泄露的真正原因。

因为 ThreadLocal 本身不存储对象,是调用 Thread 中的 ThreadLocalMaps 来保存,而 Thread 强引用 ThreadLocalMaps 对象,如果 Thread 对象生命周期过长,不能及时被回收,就会导致 ThreadLocalMaps 对象里 Entry 的 value 存在内存泄露的可能

当然 ThreadLocalMaps 在设计的时候也考虑过这个问题,所以 ThreadLocalMaps的Key 采用了弱引用的方式,并且在 set、remove、rehash 的时候会主动清理 ThreadLocalMaps 中 Key 为 Null 的 value,但是如果 ThreadLocal 已经不被使用了,set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么一ThreadLocalMaps 中的 Value 强引用就会一直存在,也就避免不了 Value 的内存泄漏。

弱引用一般是用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象

实际开发中,当我们不需要 threadlocal 后,为了 GC 将 threadlocal 变量置为 null,没有任何强引用指向堆中的 threadlocal 对象时,堆中的 threadlocal 对象将会被 GC 回收,假设现在 Key 持有的是 threadlocal 对象的强引用,如果当前线程仍然在运行,那么从当前线程一直到 threadlocal 对象还是存在强引用,由于当前线程仍在运行的原因导致 threadlocal 对象无法被 GC,这就发生了内存泄漏 。相反,弱引用就不存在此问题,当栈中的 threadlocal 变量置为 null 后,堆中的 threadlocal 对象只有一个 Key 的弱引用关联,下一次 GC 的时候堆中的 threadlocal 对象就会被回收,使用弱引用对于 threadlocal 对象而言是不会发生内存泄漏的

那为什么不把value也设置为弱引用呢?这样不就可以和key一起回收了

假设 key 所引用的 ThreadLocal 对象还被其他的引用对象强引用着,那么这个 ThreadLocal 对象就不会被 GC 回收,但如果 value 是弱引用且不被其他引用对象引用着,那 GC 的时候就被回收掉了,那线程通过 ThreadLocal 来获取 value 的时候就会获得 null,显然这不是我们希望的结果。因为对我们来说,value 才是我们想要保存的数据,ThreadLocal 只是用来关联 value 的,如果 value 都没了,还要 ThreadLocal 干嘛呢?所以 value 不能是弱引用。

key是自变量,value是因变量,所以应该变key,不要主动变value。更重要的原因是key是共享的,而value是独享的,所以把value置为null后强引用就消失了,就可以被GC,就不会导致内存泄漏。把key置为null后当前线程依然强引用着key,这就导致了内存泄漏。

ThreadLocal在调用set、remove、rehash方法的时候实际上是先获得当前线程的threadlocalmap,利用map的set、remove、rehash方法来清除key为null的value。

为什么用ThreadLocal不用线程成员变量?

如果用成员变量,那么成员变量必须在Thread里,不能在Runnable里,因为一个Runnable对象可以被多个Thread执行。

而如果在Thread中添加成员变量,就要加强Thread和Runnable的耦合,将Thread作为Runnable的成员变量,并在Runnable中调用具体的Thread变量,如果执行Runnable的Thread可能有很多子类,不同子类有不同的成员变量,则要在run方法中进行复杂处理,扩展性较低,不利于维护。而ThreadLocal就是将成员变量统一为一个Map放到线程里。

就是变量在Thread里,但又需要在Runnable任务中执行时使用,就得传进去。

本质就是在讲,现在有a,b,c三个变量值,你需要在run方法中使用,那这三变量放哪里比较合适。

是放threadlocal还是作为thread的成员变量,这两种方式在run方法中进行使用。

然后这里探讨的是这两种方法使用上的差别。

其实使用thread成员变量的方式很少,这里实在难理解就过吧,了解下就行。

所以说实际上thread就是通过成员变量的形式来存储线程独有的数据,只是提供好了threadlocal这样完善的一个线程数据存取工具,隐藏了保证线程间的隔离性的具体细节等,我们直接通过threadlocal来进行存取。是这样子吗

解释一下ThreadLocal

ThreadLocal 是 Java 中用于为每个线程提供独立变量副本的类。每个线程都可以独立地访问和修改这个变量,而不会影响其他线程的值。这在多线程编程中非常有用,特别是在需要线程隔离的场景下,ThreadLocal 可以提供一种简便的方式来避免线程间共享变量引起的并发问题。

1. ThreadLocal 的基本概念

ThreadLocal 类允许我们为每个线程存储独立的变量副本。每个线程可以通过 ThreadLocal 的实例来获取和修改自己独立的变量副本,而不用担心其他线程的干扰。

  • 每个线程都有自己的局部变量副本,不同线程之间的副本相互独立。
  • ThreadLocal 提供了线程安全的变量存储机制,避免了传统同步机制的复杂性。

2. 工作原理

ThreadLocal 的工作原理是基于每个线程维护的一个独立的 ThreadLocalMap 对象。这个 ThreadLocalMap 保存在每个线程的 Thread 实例中,它用 ThreadLocal 作为键,用线程对应的值作为值来存储每个线程的变量副本。

具体步骤:

  1. 当一个线程第一次调用 ThreadLocal.set() 时,线程会在自己的 ThreadLocalMap 中存储该 ThreadLocal 对象和对应的值。
  2. 当同一线程再次调用 ThreadLocal.get() 时,线程会从 ThreadLocalMap 中查找对应的值。
  3. 不同线程都有自己的 ThreadLocalMap,因此每个线程访问和修改 ThreadLocal 中的值都是独立的,互不影响。

3. ThreadLocal 的常用方法

  • set(T value): 设置当前线程的本地变量副本的值。
  • get(): 获取当前线程的本地变量副本的值。如果该线程是第一次调用 get(),且没有调用过 set() 方法,则返回 initialValue() 方法的返回值(默认是 null,除非重写 initialValue() 方法)。
  • remove()移除当前线程的本地变量副本,避免内存泄漏。
  • initialValue()用于返回当前线程的初始值。可以通过重写此方法来为每个线程提供默认值。

4. 使用示例

以下是一个使用 ThreadLocal 来存储每个线程独立变量的简单示例:

public class ThreadLocalExample {

    // 创建一个 ThreadLocal 对象,用于存储每个线程的独立变量副本。注意没有new!
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        // 创建两个线程,每个线程都会独立地操作自己的 threadLocalValue
        Thread thread1 = new Thread(() -> {
            threadLocalValue.set(100);
            System.out.println(Thread.currentThread().getName() + " initial value: " + threadLocalValue.get());
            threadLocalValue.set(threadLocalValue.get() + 1);
            System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocalValue.get());
        });

        Thread thread2 = new Thread(() -> {
            threadLocalValue.set(200);
            System.out.println(Thread.currentThread().getName() + " initial value: " + threadLocalValue.get());
            threadLocalValue.set(threadLocalValue.get() + 1);
            System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocalValue.get());
        });

        thread1.start();
        thread2.start();
    }
}
输出示例
Thread-0 initial value: 100
Thread-0 updated value: 101
Thread-1 initial value: 200
Thread-1 updated value: 201

在上面的示例中,两个线程 thread1thread2 各自有自己独立的 threadLocalValue,它们的值相互独立,互不影响。ThreadLocal 确保每个线程有自己的变量副本。

5. 典型使用场景

ThreadLocal 通常用于以下场景:

5.1 数据库连接或会话管理

在数据库操作时,ThreadLocal 可以用来管理每个线程的数据库连接,使得每个线程使用自己的数据库连接,而不是共享同一个连接,避免了线程安全问题。

Spring提供了事务相关的操作,而我们知道事务是得保证一组操作同时成功或失败的。这意味着我们一次事务的所有操作需要在同一个数据库连接上,但是在我们日常写代码的时候是不需要关注这点的。Spring就是用的ThreadLocal来实现,ThreadLocal存储的类型是一个Map,Map中的key 是DataSource,value 是Connection(为了应对多数据源的情况,所以是一个Map),用了ThreadLocal保证了同一个线程获取一个Connection对象,从而保证一次事务的所有操作需要在同一个数据库连接上

public class DatabaseConnectionManager {

    private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
        // 初始化数据库连接
        return createNewConnection();
    });

    public static Connection getConnection() {
        return connectionHolder.get();
    }
}
5.2 用户上下文

在 web 应用中,ThreadLocal 可以用来存储每个线程的用户上下文信息,例如用户的 sessionrequest 对象,这样可以避免显式传递这些信息。

public class UserContext {
    private static ThreadLocal<String> currentUser = new ThreadLocal<>();

    public static void setCurrentUser(String user) {
        currentUser.set(user);
    }

    public static String getCurrentUser() {
        return currentUser.get();
    }
}
5.3 避免参数传递

有时在需要多层方法调用中传递数据,可以通过 ThreadLocal 来避免通过方法参数显式传递数据。每个线程可以在调用链中共享 ThreadLocal 中的数据。

6. ThreadLocal 的内存泄漏问题

ThreadLocal 存在潜在的内存泄漏风险,尤其是在使用线程池的场景下。由于线程池中的线程会被重复使用,ThreadLocal 对象可能会继续持有某些线程的局部变量,即使它们已经不再需要。为了避免这种情况,应该在每次使用完 ThreadLocal 后调用 remove() 方法,显式地清除线程的本地变量。

示例:正确清理 ThreadLocal
try {
    // 使用 ThreadLocal
    threadLocalValue.set(someValue);
    // 执行逻辑
} finally {
    // 清理 ThreadLocal,避免内存泄漏
    threadLocalValue.remove();
}

7. 总结:ThreadLocal 的优缺点

优点
  • 提供了线程安全的局部变量副本,避免了使用锁的复杂性。
  • 每个线程拥有自己独立的变量,不会受到其他线程的影响,非常适合处理线程隔离的数据。
缺点
  • 如果不正确使用 remove(),在长时间运行的应用中可能会导致内存泄漏,尤其是在线程池中。
  • 使用过多的 ThreadLocal 可能使代码难以调试和维护,因为变量的值是隐式在线程内部管理的。

总结

ThreadLocal 是一种为每个线程提供独立的局部变量副本的机制,避免了线程间的共享和竞争。它适用于需要线程隔离的场景,比如数据库连接、用户会话和上下文信息。在使用 ThreadLocal 时,要注意清理工作,以防止内存泄漏。

在内存中,ThreadLocal、Thread实例、threadLocals变量和ThreadLocalMap到底是怎么存储的?它们的关系是什么?

在 Java 中,ThreadLocal 的存储机制设计得非常精妙,它通过每个 Thread 实例中的 ThreadLocalMap 来存储线程的局部变量。了解 ThreadLocalThread 实例、threadLocals 变量以及 ThreadLocalMap 的存储结构和它们的关系,有助于理解 ThreadLocal 的工作原理,尤其是如何做到每个线程有独立的局部变量。

基本关系与存储结构

ThreadLocal 实例
  • ThreadLocal 实例本身并不直接存储数据,它只是一个工具类,提供对线程局部变量的访问方式。
  • 每个 ThreadLocal 实例为当前线程的局部变量创建一个唯一的键,这个键用于访问存储在线程中的变量副本。
Thread 实例
  • 每个线程都有一个 Thread 对象实例,它包含线程的各种属性和状态信息。
  • Thread 对象中有一个 ThreadLocalMap,用于存储线程的局部变量副本。这个 ThreadLocalMapThreadLocal 机制的核心存储结构。
threadLocals 变量
  • Thread 类中,有一个名为 threadLocals 的成员变量,它的类型是 ThreadLocalMap
  • threadLocalsThreadLocalMap 的引用,每个线程都有自己独立的 ThreadLocalMap 实例,用于存储该线程的所有 ThreadLocal 变量。
ThreadLocalMap
  • ThreadLocalMap 是一个专门设计的映射结构,存储当前线程的所有 ThreadLocal 变量及其对应的值。
  • ThreadLocalMap 使用 ThreadLocal 作为键,存储值为线程的局部变量副本。
  • 其内部存储的每个键值对称为 EntryEntry 的键是弱引用的 ThreadLocal 实例,值是该线程对应的变量副本。

存储过程的解释

当一个线程第一次访问 ThreadLocal 时,它的局部变量是存储在该线程的 ThreadLocalMap 中的。这个存储过程如下:

  1. 线程启动时
    1. 每个线程启动时,JVM 会为该线程创建一个 Thread 实例。这个 Thread 实例包含一个名为 threadLocals 的字段,最初 threadLocalsnull,意味着线程尚未存储任何 ThreadLocal 变量。
  2. 第一次调用 ThreadLocal.set()
    1. 当线程第一次调用 ThreadLocal.set()ThreadLocal 先检查当前线程的 threadLocals 是否为 null
    2. 如果 threadLocalsnull,说明当前线程还没有初始化 ThreadLocalMap,此时会为该线程创建一个新的 ThreadLocalMap 实例,并将其赋值给 threadLocals
    3. 然后,ThreadLocal 使用自己作为键,将需要存储的值放入 ThreadLocalMap 中。
  3. 访问时 (ThreadLocal.get())
    1. 当线程调用 ThreadLocal.get() 获取值时,ThreadLocal 会通过当前线程的 ThreadLocalMap 查找对应的值。
    2. 通过 ThreadLocal 实例作为键,查询 ThreadLocalMap 中的 Entry,找到该线程对应的变量副本并返回。

内存中存储结构的关系

在内存中,这些对象和它们的关系如下:

  • Thread 实例:每个线程都对应一个 Thread 实例,这个实例中包含一个 ThreadLocalMap
  • ThreadLocalMap 实例ThreadLocalMapThread 的一个内部类,它存储该线程的所有 ThreadLocal 变量。ThreadLocalMap 由键值对(Entry)组成,每个 Entry 包含一个 ThreadLocal 作为键,和该 ThreadLocal 对应的值。
  • ThreadLocal 实例ThreadLocal 是存储线程局部变量的工具。每个 ThreadLocal 实例通过 ThreadLocalMap 存储或获取当前线程的变量副本。
  • 存储在 ThreadLocalMap 中的 Entry
    • Entry 的键是 ThreadLocal 的弱引用,值是当前线程的变量副本。
    • 键为弱引用意味着,当 ThreadLocal 对象不再使用时,GC 可以回收这个引用,避免内存泄漏。
示意图
+------------------+
|     Thread       |   Thread 1
|                  |-------------------> threadLocals (ThreadLocalMap)
| threadLocals     |                              |
+------------------+                              |
                                                  v
                                            +-----------------+
                                            |   ThreadLocalMap |
                                            |   Entry[]        |
                                            +-----------------+
                                            | Key: ThreadLocal |--> Weak Reference
                                            | Value: data      |
                                            +-----------------+
                                            
+------------------+
|     Thread       |   Thread 2
|                  |-------------------> threadLocals (ThreadLocalMap)
| threadLocals     |                              |
+------------------+                              |
                                                  v
                                            +-----------------+
                                            |   ThreadLocalMap |
                                            |   Entry[]        |
                                            +-----------------+
                                            | Key: ThreadLocal |--> Weak Reference
                                            | Value: data      |
                                            +-----------------+

具体示例解释

public class ThreadLocalExample {
    // threadLocal 就是ThreadLocal实例,也就是ThreadLocalMap中的Entry中的key。
    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
            threadLocal.set("Thread 1 Value");
            System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
            threadLocal.set("Thread 2 Value");
            System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
        });

        thread1.start();
        thread2.start();
    }
}
执行过程
  1. 线程 1 启动时:
    1. threadLocal.get() 会初始化线程的 ThreadLocalMap,并设置初始值 "Initial Value"ThreadLocalMapThreadLocal 实例作为键,"Initial Value" 作为值存入 Entry 中。
    2. 然后调用 threadLocal.set("Thread 1 Value"),此时线程 1 的 ThreadLocalMap 会将值更新为 "Thread 1 Value"
  2. 线程 2 启动时:
    1. threadLocal.get() 会为线程 2 创建自己的 ThreadLocalMap,并存储 "Initial Value"
    2. 然后调用 threadLocal.set("Thread 2 Value"),更新其 ThreadLocalMap 中的值为 "Thread 2 Value"

每个线程的 ThreadLocalMap 是独立的,它们之间互不干扰。

内存泄漏风险与解决方案

由于 ThreadLocalMap 使用 ThreadLocal 的弱引用作为键,一旦 ThreadLocal 对象没有被引用时,GC 会回收该 ThreadLocal,但对应的值如果没有被清理,可能会导致内存泄漏。

  • 内存泄漏的风险:当 ThreadLocal 被回收后,ThreadLocalMap 中的值可能仍然存在,无法被 GC 清理,从而导致内存泄漏。
  • 解决方案:在使用完 ThreadLocal 后,应该主动调用 remove() 方法,显式地清理 ThreadLocalMap 中的值,避免内存泄漏。
threadLocal.remove(); // 显式清除,防止内存泄漏

总结

  • ThreadLocal 是为每个线程提供独立变量副本的机制,解决了多线程并发访问时的共享数据问题。
  • Thread 实例 包含一个 ThreadLocalMap,用于存储该线程的所有 ThreadLocal 变量。
  • ThreadLocalMap 使用 ThreadLocal 的弱引用作为键,值是线程的局部变量副本。
  • 为防止内存泄漏,应该在适当的时候调用 ThreadLocal.remove() 方法清理资源。

刚初始化完ThreadLocalMap时,ThreadLocalMap中有几个Entry?

ThreadLocalMap 初始化完成时,ThreadLocalMap 中的 Entry 数量为 10,具体情况取决于 ThreadLocal 的使用方式。

  • 如果在初始化 ThreadLocalMap 时,已经调用了 ThreadLocal.set()ThreadLocal.withInitial(),那么 ThreadLocalMap 中会包含 1 个 Entry,这个 Entry 存储的是当前线程的 ThreadLocal 实例作为键,以及对应的初始值作为值。
  • 如果只创建了 ThreadLocal 实例,但没有调用 set()withInitial()ThreadLocalMap 并不会立即初始化。因此,ThreadLocalMap 为空,此时 ThreadLocalMap 中没有 Entry

详细解释

调用 ThreadLocal.withInitial() ThreadLocal.set() 的情况

如果你在使用 ThreadLocal 时调用了 ThreadLocal.withInitial()ThreadLocal.set(),会触发 ThreadLocalMap 的初始化过程。此时,ThreadLocalMap 中会包含一个 Entry,存储当前线程的 ThreadLocal 实例和它对应的初始值。

public class ThreadLocalExample {
    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(threadLocal.get());
        });
        thread.start();
    }
}

在这个例子中:

  • ThreadLocal.withInitial() 被调用时,ThreadLocal 会初始化 ThreadLocalMap,并将 "Initial Value" 存储到当前线程的 ThreadLocalMap 中。
  • 因此,ThreadLocalMap 中会有 1 个 Entry
未调用 ThreadLocal.set() withInitial() 的情况

如果你只是创建了 ThreadLocal 实例,但没有调用 set()withInitial(),则 ThreadLocalMap 不会立即被创建,直到你显式调用 ThreadLocal.set()get() 时,ThreadLocalMap 才会被初始化。此时,如果你从未设置任何值,ThreadLocalMap 为空。

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

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(threadLocal.get());  // 没有初始化,输出 null
        });
        thread.start();
    }
}

在这个例子中:

  • 因为没有调用 ThreadLocal.set()withInitial()ThreadLocalMap 在没有显式初始化时不会创建。
  • 调用 threadLocal.get() 时,返回 nullThreadLocalMap 中没有 Entry

总结

  • 如果调用了 ThreadLocal.set()ThreadLocal.withInitial()ThreadLocalMap 会在第一次调用时被初始化,并包含 1 个 Entry
  • 如果没有调用 set()withInitial()ThreadLocalMap 中没有 Entry,初始化时为空。

remove方法清除的是entry中的key还是value还是把整个entry清除了?

ThreadLocalremove() 方法会清除整个 Entry 中的 value,但并不会完全删除整个 Entry 对象。实际上,remove() 方法只会将 Entrykeyvalue 清除为 null,并保留 Entry 对象本身。

具体来说:

  1. 清除 Entry key
    1. ThreadLocalMap 中的 Entry 键(key)是 ThreadLocal 对象的弱引用。
    2. remove() 方法被调用时,它会清除当前线程 ThreadLocalMap 中与该 ThreadLocal 实例对应的 Entry,也就是将这个 Entry 的键设为 null
    3. 由于键是弱引用,如果 ThreadLocal 实例没有其他强引用,垃圾收集器(GC)会自动回收 ThreadLocal 对象。
  2. 清除 Entry value
    1. remove() 方法会显式将 Entry 中的 value 设为 null,以便释放这个值占用的内存,防止内存泄漏。
  3. 保留 Entry
    1. Entry 本身并不会被立即移除,而是保持在 ThreadLocalMap 数组中,等待后续的垃圾回收或 ThreadLocalMap 的整理操作。ThreadLocalMap 采用的是延迟清理机制,当发现 Entry 的键为 nullvalue 为空时,会在合适的时机清除这些无效的 Entry

源码解析

下面是 ThreadLocal 类中 remove() 方法的简化实现:

public void remove() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);  // 获取当前线程的 ThreadLocalMap
    if (map != null)
        map.remove(this);  // 在 ThreadLocalMap 中移除与当前 ThreadLocal 相关的 Entry
}

ThreadLocalMapremove() 方法大致如下:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1);  // 根据 key 计算位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.clear();   // 将当前 Entry 的 key 和 value 设为 null
            expungeStaleEntry(i);  // 清除无效的 Entry
            return;
        }
        if (k == null)  // 若发现键为 null 的 Entry,进行垃圾清理
            expungeStaleEntry(i);
    }
}
  • e.clear():清除 Entry,即将其 keyvalue 设为 null
  • expungeStaleEntry():尝试清理已经失效的 Entry,比如键为 null 的情况。

结论

  • ThreadLocal.remove() 方法会将当前线程的 ThreadLocalMap 中与该 ThreadLocal 对应的 Entry 置为 null
  • 这个操作不会立即移除整个 Entry 对象,但会标记它为可清除的。当 GC 发现 Entry 的键为 null 时,会进行清理,最终释放内存。

父子线程怎么共享数据?

在 Java 中,父线程和子线程之间的数据共享是一个常见的问题,尤其是在并发编程的场景下。尽管每个线程都有自己的独立执行路径,但有多种方式可以让父线程和子线程共享数据。以下是几种常见的实现方法:

1. 使用共享的变量

最直接的方法是让父线程和子线程访问相同的共享变量。这些共享变量可以是全局变量、静态变量、或者是在父线程中创建的对象并传递给子线程。共享变量的方式比较简单,但需要注意线程安全问题,通常需要使用同步机制(如 synchronizedvolatileLock 等)来防止数据竞争和不一致性。

示例:使用共享变量
public class SharedVariableExample {
    // 共享变量
    private static int sharedData = 0;

    public static void main(String[] args) {
        Thread parentThread = new Thread(() -> {
            // 父线程设置共享变量
            sharedData = 100;
            System.out.println("Parent Thread sets sharedData to: " + sharedData);
        });

        Thread childThread = new Thread(() -> {
            // 子线程访问共享变量
            System.out.println("Child Thread reads sharedData: " + sharedData);
        });

        parentThread.start();
        try {
            parentThread.join(); // 确保父线程执行完后再启动子线程。重点。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        childThread.start();
    }
}
线程安全注意事项

如果共享变量是可变的(如 sharedData),多个线程同时读写它可能会产生竞态条件,导致不一致或不可预料的结果。为了解决这个问题,可以使用以下同步机制:

  • synchronized 块:确保共享变量的读写操作是原子的。
  • volatile:用于保证共享变量的可见性,确保所有线程能够看到共享变量的最新值。
  • Atomic 类(如 AtomicInteger):提供了一些线程安全的操作。
private static volatile int sharedData = 0;

2. 通过构造函数传递共享对象

父线程可以通过创建一个共享对象**(如自定义的共享类或集合)并将其传递给子线程。这种方式避免了全局变量的使用**,并且可以通过控制访问权限来提高代码的可维护性。

示例:通过共享对象传递数据
public class SharedObjectExample {
    public static void main(String[] args) {
        // 共享对象
        SharedData sharedData = new SharedData();

        // 父线程设置共享对象的值
        Thread parentThread = new Thread(() -> {
            sharedData.setValue(100);
            System.out.println("Parent Thread sets value to: " + sharedData.getValue());
        });

        // 子线程读取共享对象的值
        Thread childThread = new Thread(() -> {
            System.out.println("Child Thread reads value: " + sharedData.getValue());
        });

        parentThread.start();
        try {
            parentThread.join(); // 确保父线程执行完后再启动子线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        childThread.start();
    }

    // 共享类
    static class SharedData {
        private int value;

        public synchronized void setValue(int value) {
            this.value = value;
        }

        public synchronized int getValue() {
            return value;
        }
    }
}

3. 使用 ThreadLocal

ThreadLocal 提供了一种为每个线程存储局部变量的方式。虽然 ThreadLocal 主要用于提供线程隔离的数据存储,但在某些情况下,也可以通过自定义的传递机制让父线程和子线程共享数据。

使用 InheritableThreadLocal

InheritableThreadLocalThreadLocal 的一个子类,它允许子线程继承父线程的 ThreadLocal 。当父线程在 ThreadLocal 中设置了某个值时,子线程可以直接从 InheritableThreadLocal 中获取这个值。

示例:使用 InheritableThreadLocal
public class InheritableThreadLocalExample {
    // 使用 InheritableThreadLocal 来允许子线程继承父线程的值
    private static InheritableThreadLocal<Integer> threadLocalValue = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        // 父线程设置 ThreadLocal 值
        Thread parentThread = new Thread(() -> {
            threadLocalValue.set(100);
            System.out.println("Parent Thread sets value: " + threadLocalValue.get());

            // 在父线程内启动子线程。重点。注意是启动不是只创建。
            Thread childThread = new Thread(() -> {
                System.out.println("Child Thread reads value: " + threadLocalValue.get());
            });
            childThread.start();
        });

        parentThread.start();
    }
}

在这个例子中,父线程通过 InheritableThreadLocal 设置的值被子线程自动继承,子线程可以直接读取父线程设置的值。

4. 使用阻塞队列

在生产者-消费者模型中,父线程可以作为生产者,子线程作为消费者。父线程将数据放入共享的阻塞队列中,子线程从队列中获取数据进行处理。这种方式特别适用于需要异步处理数据的场景。

示例:使用 BlockingQueue 共享数据
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueExample {
    public static void main(String[] args) {
        // 创建阻塞队列用于数据共享
        BlockingQueue<Integer> sharedQueue = new LinkedBlockingQueue<>();

        // 父线程(生产者)向队列中放数据
        Thread parentThread = new Thread(() -> {
            try {
                sharedQueue.put(100);
                System.out.println("Parent Thread put value: 100");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 子线程(消费者)从队列中取数据
        Thread childThread = new Thread(() -> {
            try {
                int value = sharedQueue.take();
                System.out.println("Child Thread took value: " + value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        parentThread.start();
        childThread.start();
    }
}

在这个例子中,父线程将数据放入阻塞队列,子线程从队列中获取数据,队列负责协调数据的传递并确保线程安全。

5. 使用 Future Callable

FutureCallable 是 Java 中用于线程任务结果的机制。父线程可以提交一个 Callable 任务给线程池或者自己创建的子线程,子线程执行任务后返回结果给父线程。

示例:使用 CallableFuture
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class FutureExample {
    public static void main(String[] args) {
        // 创建一个 Callable 子线程任务
        Callable<Integer> callableTask = () -> {
            System.out.println("Child Thread is computing...");
            return 100;
        };

        // 使用 FutureTask 来获取子线程的结果
        FutureTask<Integer> futureTask = new FutureTask<>(callableTask);

        // 启动子线程
        Thread childThread = new Thread(futureTask);
        childThread.start();

        // 父线程等待子线程的计算结果
        try {
            Integer result = futureTask.get();  // 阻塞等待子线程结果
            System.out.println("Parent Thread gets result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,父线程通过 FutureTask 提交子线程任务,并阻塞等待子线程的执行结果。

总结

父子线程之间共享数据的方式有很多种,选择合适的方法取决于具体的使用场景和需求:

  1. 共享变量:简单直接,但需要处理线程安全问题。
  2. 通过共享对象传递:将共享对象传递给子线程,适用于需要在多个线程之间共享数据的情况。
  3. ThreadLocal / InheritableThreadLocal:适用于需要为每个线程提供独立副本或传递父线程上下文的场景。
  4. 阻塞队列:适用于生产者-消费者模型,可以在父子线程之间传递任务和数据。
  5. Future Callable:适用于父线程等待子线程执行结果的场景。

选择合适的方式,既能有效地共享数据,又能保证线程安全。