Hi 大家好,我是 DHL,大厂程序员,公众号:ByteCode ,在美团、快手、小米工作过。搞过逆向,做过性能优化,研究过系统,擅长鸿蒙、Android、Kotlin、性能优化、职场分享。


ThreadLocal 可能会造成数据污染

ThreadLocal 是 Java 提供的一种机制,用于在每个线程中存储独立的变量副本。ThreadLocal 核心实现依赖于 Thread 类中的一个内部类 ThreadLocalMap

ThreadLocalMap 是一个自定义的哈希表,用于存储 ThreadLocal 变量。每个线程都有一个 ThreadLocalMap 实例,这种设计使得不同线程之间的 ThreadLocal 变量互不干扰。

这种设计的目的是为每个线程提供独立的变量副本,避免多线程环境下的竞争条件和数据共享问题。然而,这种设计在线程池中使用时,可能会造成致数据污染。

比如在使用线程池时,当一个线程在处理完一个任务后被重新用于处理另一个任务时,之前任务的 ThreadLocal 数据可能会泄漏到新的任务中,导致数据污染。

public class ThreadLocalExample {
    // 定义一个 ThreadLocal 变量
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 提交第一个任务
        executorService.submit(() -> {
            threadLocal.set("Task 1");
            System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
            // 模拟任务执行时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 提交第二个任务
        executorService.submit(() -> {
            threadLocal.set("Task 2");
            System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
            // 模拟任务执行时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 提交第三个任务,检查是否有数据污染
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
            // 清理 ThreadLocal 变量
            threadLocal.remove();
        });

        // 关闭线程池
        executorService.shutdown();
    }
}
  • 定义一个 ThreadLocal 变量,用于存储每个线程的上下文信息。
  • 使用 Executors.newFixedThreadPool(2) 创建一个固定大小为 2 的线程池
  1. 提交任务:提交三个任务到线程池中。
  • 第一个任务设置 ThreadLocal 变量为 "Task 1" 并打印。
  • 第二个任务设置 ThreadLocal 变量为 "Task 2" 并打印。
  • 第三个任务打印 ThreadLocal 变量的值,检查是否有数据污染。

到这里,我们花几秒钟思考一下,上面的代码的输出结果。

pool-1-thread-1 - Task 1
pool-1-thread-2 - Task 2
pool-1-thread-1 - Task 1

在这个例子中,第三个任务的输出可能是 "Task 1" 或 "Task 2",这取决于线程池如何复用线程。如果第三个任务复用了第一个任务的线程(pool-1-thread-1),则会输出 "Task 1",即出现数据污染。

数据污染的原因

线程池中的线程会被重复使用。如果一个线程在处理完一个任务后没有清理 ThreadLocal 变量的值,那么这个值可能会泄漏到下一个任务中。

ThreadLocal 变量在同一个线程中是共享的,除非显式调用 remove 方法,否则变量的值会一直保留在线程中。

解决方案

为了避免数据污染,可以在任务执行完毕后显式调用 ThreadLocalremove 方法,清理变量的值:

executorService.submit(() -> {
    try {
        threadLocal.set("Task 1");
        System.out.println(Thread.currentThread().getName() + " - " + threadLocal.get());
        // 模拟任务执行时间
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        threadLocal.remove(); // 清理 ThreadLocal 变量
    }
});

通过显式清理 ThreadLocal 变量,可以避免数据污染问题,确保每个任务都有一个干净的上下文环境。