ThreadLocal

什么是 ThreadLocal?

ThreadLocal(线程本地变量)是 Java 提供的一种机制,允许为每个线程维护一份独立的变量副本,从而避免了多线程环境下对同一个共享变量进行争用的风险。也就是说,对于同一个 ThreadLocal 变量,不同线程之间读写到的是完全不同的副本,线程之间互不影响。


典型应用场景

  1. 线程安全的共享数据
    在并发编程中,如果多个线程需要访问同一个实例变量时,就会涉及到同步控制,或者可能会出现线程安全问题。通过使用 ThreadLocal,可以避免使用复杂的锁操作,因为每个线程都有自己的变量副本,不会相互干扰。

    • 例如:为每个线程分配一个专有的 SimpleDateFormat 实例,用于时间格式化。避免了多线程下格式化不安全的问题,减少了加锁所带来的开销。
  2. 与拦截器/过滤器结合使用
    在 Web 应用或分布式应用中,常常需要在请求到来时设置一些上下文信息(如当前用户信息、TraceId、数据库连接等),以及在请求结束时进行清理。通过 ThreadLocal 可以将这些上下文信息与线程绑定,在整个调用链或者一次请求的生命周期内安全地存取数据。

  3. 框架内部使用
    一些框架或容器(如 Spring 的事务管理)会使用 ThreadLocal 将事务上下文或 Session 信息与当前线程绑定,实现无侵入的设计。


底层原理

在 Java 中,每个线程对象(java.lang.Thread)都持有一个 ThreadLocalMap 类型的成员变量,该 Map 的 Key 就是 ThreadLocal 实例本身,Value 则是具体的线程本地变量值。

  1. ThreadLocalMap 的存储结构
    ThreadLocalMap 是一个自定义的散列表(类似于 HashMap 的开放地址法),它的 key 并不是强引用,而是一个弱引用(WeakReference)。这样做是为了避免 ThreadLocal 对象本身长时间不被回收时,仍然持有对 value 的强引用,从而导致内存泄漏。

  2. 线程隔离
    当我们调用 threadLocal.set(value) 时,实际上是往当前线程的 ThreadLocalMap 中存储了键值对(key = ThreadLocal, value = value)。其他线程因为有自己独立的 ThreadLocalMap,自然无法访问到这个 value,也就实现了线程隔离。

  3. ThreadLocalMap 的弱引用陷阱
    由于 Key(ThreadLocal 对象)是弱引用,如果 ThreadLocal 实例本身被 GC 回收了,但线程仍在存活,Map 中就可能出现“Key 为 null”的“脏”数据。
    此时如果没有进行任何访问触发清理操作,就会残留一份“无人管理”的 value 对象,造成内存浪费。

原理图

image.png

不及时 remove 带来的后果

  1. 潜在的内存泄漏
    当使用线程池(如 Tomcat、Dubbo 等容器内置的线程池)时,线程并不会在请求结束后就结束。而是被重复复用。如果在线程结束或请求处理结束时,没有调用 ThreadLocal.remove() 或在适当的时机清理数据,剩下的 ThreadLocal 值就有可能被“遗留”在这个线程的 ThreadLocalMap 中。

    • 对于长时间存活的线程池,这些“遗留”对象可能就一直无法被回收,导致内存泄漏
  2. 数据污染
    当线程被复用时,上一笔请求/thread 执行完毕后未清理的上下文信息,有可能导致下一笔请求的线程仍能读到这份数据而产生混淆、错乱(典型如数据源切换、用户上下文错乱等)。

  3. GC 处理不及时
    虽然 ThreadLocalMap 会在下次调用 ThreadLocal 相关方法时尝试清理无效 entry,但如果业务逻辑从此再也不访问 ThreadLocal,value 可能会在相当长的一段时间内无法被清理到。

因此在使用完 ThreadLocal 后,尤其是当业务场景明确地要求在一次调用或一次请求结束后就不再使用此变量时,务必调用 remove() 方法进行清理。


本质

  • 从使用目的看,更多是一种“空间换并发安全”或说“空间换时间”

    • 多线程访问同一个对象时需要加锁,而加锁带来的开销是“时间成本”上的损失;而 ThreadLocal 让每个线程都拥有独立副本,这样就避免了锁竞争,从而提升了并发效率。但代价就是为每个线程多分配一份资源,也就是额外的“空间”开销。
  • 如果从“时间”或“空间”更一般的角度去衡量:

    • ThreadLocal 并没有减少计算过程中的时间复杂度,更多是简化了同步,降低了锁争用的时间,同时增加了内存占用(因为多个线程有多份副本)。因此从宏观来看,可以说 ThreadLocal 是用额外的空间(多份副本)换取更少的竞争同步时间

总结

  1. ThreadLocal 的作用

    • 为每个线程提供独立的变量副本,从而简化并发编程,避免锁争用。
  2. 应用场景

    • 常见于需要线程独立的上下文变量(如日期格式化器、用户信息、事务上下文等)的存储。
  3. 底层原理

    • 每个线程持有一个 ThreadLocalMap,内部以 ThreadLocal 作为弱引用 key 存储变量值。
  4. 不及时 remove 的后果

    • 在线程池环境下可能造成内存泄漏、数据污染。
  5. “空间换时间”

    • 通过为每个线程额外分配副本的方式,减少了线程间的争用和同步,从而提高了并发性能,但付出了额外的空间成本。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

推荐阅读更多精彩内容