ThreadLocal 的原理和实现
ThreadLocal 变量即线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map,这个 Map 不是直接使用的 HashMap,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap 类对应的 get()、set() 方法。这个储值的 Map 并非 ThreadLocal 的成员变量,而是 java.lang.Thread 类的成员变量。ThreadLocalMap 实例是作为 java.lang.Thread 的成员变量存储的,每个线程有唯一的一个 threadLocalMap。
这个 map 以 ThreadLocal 对象为 key,'线程局部变量'为值,所以一个线程下可以保存多个'线程局部变量'。对 ThreadLocal 的操作,实际委托给当前 Thread,每个 Thread 都会有自己独立的 ThreadLocalMap 实例,存储的仓库是 Entry[] table;Entry 的 key 为 ThreadLocal,value 为存储内容;因此在并发环境下,对 ThreadLocal 的 set 或 get,不会有任何问题。
由于 Tomcat 线程池的原因,最初使用的'线程局部变量'保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值。所以在线程池的情况下,处理完成后主动调用该业务 ThreadLocal 的 remove() 方法,将'线程局部变量'清空,避免本线程下次处理的时候依然存在旧数据。
ThreadLocal 为什么要使用弱引用和内存泄露问题
在 ThreadLocal 中内存泄漏是指 ThreadLocalMap 中的 Entry 中的 key 为 null,而 value 不为 null。因为 key 为 null 导致 value 一直访问不到,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,value 可达从而不会被回收掉,但是该 value 永远不能被访问到,这样就存在了内存泄漏。
如果 key 是强引用,那么发生 GC 时 ThreadLocalMap 还持有 ThreadLocal 的强引用,会导致 ThreadLocal 不会被回收,从而导致内存泄漏。弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 方法时被清除,这算是最优的解决方案。
Map 中的 key 为一个 threadlocal 实例。如果使用强引用,当 ThreadLocal 对象(假设为 ThreadLocal@123456)的引用被回收了,ThreadLocalMap 本身依然还持有 ThreadLocal@123456 的强引用,如果没有手动删除这个 key,则 ThreadLocal@123456 不会被回收,所以只要当前线程不消亡,ThreadLocalMap 引用的那些对象就不会被回收,可以认为这导致 Entry 内存泄漏。
如果使用弱引用,那指向 ThreadLocal@123456 对象的引用就两个:ThreadLocal 强引用和 ThreadLocalMap 中 Entry 的弱引用。一旦 ThreadLocal 强引用被回收,则指向 ThreadLocal@123456 的就只有弱引用了,在下次 gc 的时候,这个 ThreadLocal@123456 就会被回收。
虽然上述的弱引用解决了 key,也就是线程的 ThreadLocal 能及时被回收,但是 value 却依然存在内存泄漏的问题。当把 threadlocal 实例置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。map 里面的 value 却没有被回收。而这块 value 永远不会被访问到了。
所以存在着内存泄露,因为存在一条从 current thread 连接过来的强引用。只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开,Current Thread, Map, value 将全部被 GC 回收。所以当线程的某个 localThread 使用完了,马上调用 threadlocal 的 remove 方法,就不会发生这种情况了。
另外其实只要这个线程对象及时被 gc 回收,这个内存泄露问题影响不大,但在 threadLocal 设为 null 到线程结束中间这段时间不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用,就可能出现内存泄露。


