ThreadLocal 是什么

ThreadLocal 提供线程局部变量,这些变量与普通变量的不同之处在于,访问这些变量的每个线程(通过 ThreadLocal 的 get 或 set 方法)都有自己的、独立初始化的变量副本。
ThreadLocal 实例通常指代希望与线程关联的类中的私有静态字段。例如,我希望为每个线程分配一个 uuid,我会这么写:

private static final ThreadLocal<UUID> threadId =
          new ThreadLocal<UUID>() {
              @Override protected UUID initialValue() {
                  return UUID.randomUUID();
              }
          };

或者:

private static final ThreadLocal<UUID> threadId = 
          ThreadLocal.withInitial(UUID::randomUUID);

在上例中,如果某个线程第一次通过 threadId.get() 获取 uuid,则会触发初始化分配一个独立的 uuid,不同的线程无法访问对方的 uuid。

ThreadLocal 如何实现的?

ThreadLocal 的 get() 方法本质上是通过哈希表的方式获取线程隔离的数据,但它不是直接使用 HashMap,而是依赖储存在每个线程内部的特殊数据结构 —— ThreadLocalMap,这是一个以 ThreadLocal 为 key(弱引用)、实际数值为 value(强引用)的哈希表数据结构。
ThreadLocal 实例本身只是字段的指代,并不储存实际值。因此不需要对它使用 volatile 关键字。
以下流程图展示了 ThreadLocal.get() 调用后的大致流程:

ThreadLocal 流程图.png

为什么使用 ThreadLocal?

在上文中,我们用 ThreadLocal 给每个线程分配了独立的 uuid。如果我们不使用 ThreadLocal,而是使用一个 ConcurrentHashMap,虽然节约了空间,但是产生了加锁的开销。
一些 ThreadLocal 的应用场景:

  1. 在使用数据库连接池时,可以通过 ThreadLocal 为每个线程绑定一个独立的数据库连接。
  2. 同线程多流程业务中,将上下文信息存于 ThreadLocal 中,避免使用公共资源。

ThreadLocal 内存泄漏问题

ThreadLocalMap 可能会发生内存泄露,尤其是在使用线程池时。ThreadLocalMap 对 ThreadLocal 是弱引用,但对值本身是强引用。如果线程消亡,垃圾回收时会自动回收该线程的 ThreadLocalMap,不会发生内存泄露。但是线程池中的线程不会消亡,其中,如果对于键的引用被回收,值将无法被访问,也无法解除引用,这就发生了内存泄露。
为避免这种问题,要在多线程业务结束时显式调用 ThreadLocal.clear();