前言

ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:
- 存储单个线程上下文信息。比如存储id,登录用户信息,白名单等;
- 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
- 减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。

  1. public class ThreadLocalDemo {
  2. ThreadLocal<ArrayList<String>> testThreadLocal = new ThreadLocal<>();
  3. }

原理

ThreadLocal储存的变量,其实是放入了当前Thread里。每个Thread都有一个成员变量inheritableThreadLocals,它的类型是ThreadLocal**.**ThreadLocalMap,是一个map,是ThreadLocal的内部类。

  1. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

这个map的entry是ThreadLocalMap的内部类Entry,具体的key和value类型分别是ThreadLocal和Object。当设置一个ThreadLocal变量时testThreadLocal.set("str"),这个map里就多了一对ThreadLocal -> Object的映射。

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

每当Stack栈中创建一个ThreadLocal变量时,会在Heap堆中生成一个ThreadLocal的实例,同时,会将这个实例的引用作为key存入线程Thread中的TreadLocalMap中,此时entry的key为ThreadLocal的refrence引用, value为null,当调用ThreadLocalset(Object o)方法后会将传入的值存入entry的value中,如图所示
ThreadLocal原理及内存泄露预防 - 图1

为什么不用线程ID作为KEY?

一个线程内可能定义多个ThreadLocal变量,当我们想取出ThreadLocal中的变量时调用get()方法,线程会从Thread中获取TreadLocalMap,根据ThreadLocal获取对应的value,所以当有多个ThreadLocal实例时,我们可以获取对应ThreadLocal保存的value,如果使用线程ID作为KEY无法区分各个ThreadLocal

为什么key使用弱引用?

由上图可知一个ThreadLocal对象最少被2个变量持有,一个是他自身的变量,一个是当前线程Thread。
如果使用强引用,当ThreadLocal对象生命周期结束被回收了,线程内的ThreadLocalMap依然还持有他强引用,如果没有手动删除这个key,则ThreadLocal对象不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。
而如果使用弱引用,在ThreadLocal对象被回收后,Thread.ThreadLocalMap持有的仅仅是ThreadLocal的弱引用,在gc扫描到后会被直接回收,然而这也导致了一个问题,key作为弱引用被回收,value却没有被回收,而且key变成null后value更是无法被访问到了!
针对这一问题,ThreadLocalMap类的设计本身已经有了这一问题的解决方案,那就是在每次get()/set()/remove()ThreadLocalMap中的值的时候,会自动清理key为null的value。如此一来,value也能被回收了。
既然对key使用弱引用,能使key自动回收,那为什么不对value使用弱引用?答案显而易见,假设往ThreadLocalMap里存了一个value,gc过后value便消失了,那就无法使用ThreadLocalMap来达到存储全线程变量的效果了。

内存泄露

总结一下内存泄露(本该回收的无用对象没有得到回收)的原因:

  • static

通常,我们需要保证作为key的ThreadLocal变量能够被全局访问到,同时也必须保证其为单例,因此,在一个类中将其设为static类型便成为了惯用做法。如果使用static修饰,每当ThreadLocal所在的类创建了一个实例时,作为成员变量的ThreadLocal也会创建一个实例,例如ThreadLocal@487ThreadLocal@488,两者之间并不能共享变量。
但是当ThreadLocal对象的引用变量被static修饰时,只要jvm不停static变量就不会清除,弱引用形同虚设,所以当某个ThreadLocal变量不再使用时,记得调用remove()方法删除该key。

  • 线程池

使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。