ThreadLocal详解(原理、使用、内存泄漏原因)

247 阅读4分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!

基本概念

ThreadLocal 为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。可以把 ThreadLocal 理解成一个线程级别的数据存储容器,每个线程都有自己专属的存储空间,不同线程之间的数据是相互隔离的。

原理

数据存储结构

ThreadLocal 的原理基于 Thread 类中的一个 ThreadLocalMap 成员变量。ThreadLocalMap 是 ThreadLocal 类的一个静态内部类,它类似于 HashMap,以 ThreadLocal 对象作为键,以线程局部变量的值作为值。每个 Thread 对象都有自己的 ThreadLocalMap 实例,用于存储该线程的所有 ThreadLocal 变量及其对应的值。

操作流程

  • set 方法:当调用 ThreadLocal 的 set 方法时,首先会获取当前线程的 ThreadLocalMap 实例。如果 ThreadLocalMap 存在,则将当前 ThreadLocal 对象作为键,要设置的值作为值,存入 ThreadLocalMap 中;如果 ThreadLocalMap 不存在,则创建一个新的 ThreadLocalMap 并将键值对存入其中。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  • get 方法:当调用 ThreadLocal 的 get 方法时,同样会先获取当前线程的 ThreadLocalMap 实例。如果 ThreadLocalMap 存在,则根据当前 ThreadLocal 对象作为键去查找对应的值;如果 ThreadLocalMap 不存在或者没有找到对应的值,则调用 initialValue 方法返回初始值。
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();
}

使用场景

解决线程安全问题

当多个线程需要使用同一个对象,但又不希望出现线程安全问题时,可以使用 ThreadLocal 为每个线程提供一个独立的对象副本。例如,在多线程环境下使用 SimpleDateFormat 进行日期格式化时,由于 SimpleDateFormat 不是线程安全的,使用 ThreadLocal 可以为每个线程创建一个独立的 SimpleDateFormat 实例,避免线程安全问题。

import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalDateFormat {
    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    public static String formatDate(Date date) {
        return dateFormatThreadLocal.get().format(date);
    }
}

保存线程上下文信息

在一个线程的执行过程中,可能会涉及多个方法的调用,有些信息需要在整个线程的执行过程中共享。可以使用 ThreadLocal 来保存这些上下文信息,方便在不同的方法中获取。例如,在一个 Web 应用中,可以使用 ThreadLocal 保存当前用户的信息,在整个请求处理过程中都可以方便地获取。父子线程数据传递问题:

  • InheritableThreadLocal 的局限性InheritableThreadLocal 用于在父线程创建子线程时将父线程的 ThreadLocal 值传递给子线程。但它只能在子线程创建时进行一次传递,后续父线程对 ThreadLocal 值的修改不会影响到子线程。而且在使用线程池时,由于线程是复用的,InheritableThreadLocal 可能无法满足动态更新子线程数据的需求。
  • 第三方解决方案:为了解决父子线程及线程池环境下的数据传递问题,出现了一些第三方库,如 Alibaba 的 TransmittableThreadLocal(TTL)。它通过重写 ThreadLocal 的相关方法,结合 AOP 等技术,实现了在复杂线程环境下(包括线程池)数据的正确传递和动态更新。
public class UserContextHolder {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    public static void clear() {
        userThreadLocal.remove();
    }
}

优缺点

优点

  • 线程隔离ThreadLocal 为每个线程提供独立的变量副本,不同线程之间的数据相互隔离,避免了多线程之间的竞争和同步问题,提高了程序的安全性和性能。
  • 使用方便:使用 ThreadLocal 可以很方便地在不同的方法中共享数据,无需通过参数传递,简化了代码的编写。

缺点

  • 内存泄漏问题:由于 ThreadLocalMap 中的键是 ThreadLocal 对象的弱引用,而值是强引用。当 ThreadLocal 对象被垃圾回收后,ThreadLocalMap 中可能会存在键为 null 的条目,但值仍然存在,这些条目无法被访问到,却不会被自动回收,从而导致内存泄漏。为了避免内存泄漏,在使用完 ThreadLocal 后,应该及时调用 remove 方法清除数据。

image.png

  • 增加内存开销:每个线程都有自己的 ThreadLocalMap,如果使用大量的 ThreadLocal 变量,会增加内存的开销。

总结

ThreadLocal 是 Java 中一个非常有用的工具,它提供了一种简单而有效的方式来实现线程级别的数据隔离。在使用 ThreadLocal 时,需要注意内存泄漏问题,及时清除不再使用的数据。同时,要根据实际情况合理使用,避免过度使用导致内存开销过大。