ThreadLocal 为什么要这么实现?
多线程环境下,编程需要考虑并发安全问题。常规思路是通过锁来实现,ThreadLocal 则提供了另一种线程安全的实现方式:避免共享。
通过 ThreadLocal 定义的变量,不同的线程都会拥有这个变量的副本,没有共享,也就没有并发问题了。
具体的使用方式可以看 JDK 源码中提供的示例,下面这个类中,定义了一个静态私有的 ThreadLocal 变量,为每个线程生成唯一标识。当一个线程首次调用 get 方法时,会调用初始化方法 initialValue,为当前线程分配唯一的 thread id,并且在之后同一个线程再次调用 get 方法时,直接返回这个值。
这里区分两个概念,一个是 ThreadLocal 变量,一个是 ThreadLocal 变量“包裹”的对象(在这个例子中是一个 Integer 对象)。准确来说,ThreadLocal 变量是唯一的,不同线程持有的是 ThreadLocal “包裹”的对象的副本。
1 | public class ThreadId { |
实现原理
介绍 ThreadLocal 是如何实现的前,可以尝试自己来实现这样的效果。很容易想到,可以在 ThreadLocal 内创建一个 Map,key 是线程,value 是线程持有的变量。示意图如下所示。
ThreadLocal 持有 Map
但实际上 Java 中的实现,这个 Map 是保存在 Thread 对象中的,key 是 ThreadLocal,value 是线程持有的变量,示意图如下所示,核心代码实现也很简单。
Thread 持有ThreadLocal Map
1 | public class ThreadLocal<T> { |
Java 中的实现和自定义实现,哪种方式更好呢,显然 Java 的实现更好一点,这里我总结了一下几点优势,这也是为什么 Java 要这样实现 ThreadLocal 的原因。
- ThreadLocal 仅仅作为工具类,不持有任何线程相关的数据,而是将这些数据保存在 Thread 对象中,设计上更加容易理解
- 自定义实现中使用需要使用并发安全的 Map 来存放数据,存在锁竞争,而 Java 实现中每个线程对各自数据的读写都是独立的,没有并发问题,性能更好
- 自定义实现中,ThreadLocal 对象持有线程引用,通常 ThreadLocal 对象的生命周期要更长,如果线程没有主动删除对应的 key-value,会导致 Thread 对象无法被回收,发生内存泄漏
内存泄漏
前面我们提到 Java 这样实现 ThreadLocal 是有助于避免内存泄漏的,因为 Thread 对象持有线程本地数据,当线程销毁后,它所持有的对象副本也会被 GC 回收。
但前面我们忽略了一点,因为程序中往往是通过线程池来使用线程的,所以线程的生命周期可能也非常长。所以 Thread 对象中持有线程本地数据时,也可能导致内存泄漏。
所以 Java 实现中还做了一项优化,就是 ThreadLocalMap 中的 Entry 实现为弱引用,通过弱引用指向 key,也就是 ThreadLocal 变量,这样当程序中声明的 ThreadLocal 变量声明周期结束后,因为这里是弱引用,所以可以被顺利回收。
1 | static class ThreadLocalMap { |
那是否 Java 实现的 ThreadLocal 一定不会发生内存泄漏呢?不是的,因为 ThreadLocalMap 中 Entry 的 value 并不是弱引用,如果程序中没有及时将 value 从 map 中移除,即使 value 的生命周期结束了,也无法被回收,这里还是会发生内存泄漏。为了避免发生这种情况,我们需要在代码中手动释放 Entry 对 value 的强引用,使用示例如下:
1 | ExecutorService es; |
应用场景
了解了 ThreadLocal 的实现原理,不知道你会不会有这样的疑问,在什么情况下才需要用到 ThreadLocal 呢?如果只是为了避免共享,我们直接将要用的变量声明为局部变量不可以吗?
答案是不可以,考虑这样两种场景:
- 这个对象创建过程非常耗时、耗资源,如果每次使用时都重新创建可能导致性能问题
- 这个对象只能在某个方法中获取,但需要在很多方法中使用,如果声明为局部变量,则需要在整个链路中所有方法中进行传递,如果这个变量和业务逻辑没有紧密联系,会非常影响代码可读性、可维护性
所以,对于一些线程间不需要共享,但在同一线程,多个方法调用中都可能使用到的数据,如果这些数据获创建程很耗资源或者和业务逻辑关系不紧密,那我们就可以把它们放在 ThreadLocal 中。
看几个具体的例子:
- 程序中经常会用 traceId 把一次用户请求在系统中调用的路径串联起来,traceId 信息非常适合通过 ThreadLocal 保存
- 用户登陆系统后,针对用户的每次请求,可以从 Session 或者Token 中获取用户信息,保存到 ThreadLocal 中,在需要使用的时候可以很方便的获取到
- 从数据库连接池获取链接后,Connection 对象放到 ThreadLocal 中,Spring 的事务管理,底层实现就利用的这样的机制
使用 ThreadLocal 时,还需要特别注意以下几种情况:
- 配合线程池使用时,要知道线程池中的线程生命周期会非常长,会在不同的请求间复用,所以一定要主动 remove 使用完毕的 ThreadLocal 变量,防止内存泄漏,也防止对下次请求造成影响
- 异步调用中,ThreadLocal 是无法传递的,要避免在这样的场景中依赖 ThreadLocal 变量,或者手动实现 ThreadLocal 的跨线程传递
InheritableThreadLocal
Java 中还提供了一个 InheritableThreadLocal 类来支持父子线程之间线程本地存储变量的继承,InheritableThreadLocal 是 ThreadLocal 的子类,之所以能实现继承特性,其实是在初始化线程时,有一步操作,专门将父线程的成员变量 inheritableThreadLocals 指向的 ThreadLocalMap 中的变量拷贝到了子线程中。
1 | public class Thread { |
总结
并发编程中,ThreadLocal 通过避免共享的方式实现了线程安全。Java 中 ThreadLocal 的实现,是将线程本地数据通过 ThreadLocalMap 放在的 Thread 对象中,ThreadLocal 变量做 key,ThreadLocal “包裹”的对象副本做 value。并且 ThreadLocalMap 的 Entry 实现为弱引用,指向ThreadLocal 变量,防止内存泄漏。但是要想完全避免内存泄漏,使用时要记得及时释放对 value 的强引用。最后,要记得全链路 traceId,登陆用户信息,Spring 事务中的数据路链接等典型的应用场景