1. 造成内存泄漏的原因?

内存泄漏 memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

threadLocal 是为了解决对象不能被多线程共享访问的问题,通过 threadLocal.set 方法将对象实例保存在每个线程自己所拥有的 threadLocalMap 中,这样每个线程使用自己的对象实例,彼此不会影响达到隔离的作用,从而就解决了对象在被共享访问带来线程安全问题。

如果将同步机制和 threadLocal 做一个横向比较的话,同步机制就是通过控制线程访问共享对象的顺序,而threadLocal 就是为每一个线程分配一个该对象,各用各的互不影响。

打个比方说,现在有 100 个同学需要填写一张表格但是只有一支笔,同步就相当于A使用完这支笔后给B,B使用后给C用……老师就控制着这支笔的使用顺序,使得同学之间不会产生冲突。

而threadLocal就相当于,老师直接准备了100支笔,这样每个同学都使用自己的,同学之间就不会产生冲突。

很显然这就是两种不同的思路:

  • 同步机制以「时间换空间」,由于每个线程在同一时刻共享对象只能被一个线程访问造成整体上响应时间增加,但是对象只占有一份内存,牺牲了时间效率换来了空间效率即「时间换空间」。
  • 而threadLocal,为每个线程都分配了一份对象,自然而然内存使用率增加,每个线程各用各的,整体上时间效率要增加很多,牺牲了空间效率换来时间效率即「空间换时间」。

threadLocalthreadLocalMapentry 之间的关系如下图所示:
image.png
内存泄露造成的原因:
**上图中,实线代表强引用,虚线代表的是弱引用

  • 当 threadLocal 外部强引用被置为 null (**threadLocalInstance=null**),那么系统 GC 的时候,根据可达性分析,这个 threadLocal 实例就没有任何一条链路能够引用到它,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap中就会出现key为 null 的 Entry(entry 是强引用),就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:**Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value** 永远无法回收,造成内存泄漏。
  • 当然,如果当前 thread 运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的。

2. 已经做了哪些改进

从以上 setgetEntryremove方法看出,在 threadLocal 的生命周期里,针对 threadLocal 存在的内存泄漏的问题,都会通过 **expungeStaleEntry****cleanSomeSlots****replaceStaleEntry**这三个方法清理掉 key 为 null 的脏 entry
image.png

具体源码分析:从源码深入详解ThreadLocal内存泄漏问题

3. 为什么使用弱引用

从文章开头通过 threadLocal,threadLocalMap,entry 的引用关系看起来 threadLocal 存在内存泄漏的问题似乎是因为 threadLocal 是被弱引用修饰的。那为什么要使用弱引用呢?

如果使用强引用

假设 threadLocal 使用的是强引用,在业务代码中执行 threadLocalInstance==null 操作,以清理掉threadLocal 实例的目的,但是因为 threadLocalMap 的 Entry 强引用 threadLocal,因此在 gc 的时候进行可达性分析,threadLocal 依然可达,对 threadLocal 并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误

如果使用弱引用

假设 Entry 弱引用 threadLocal,尽管会出现内存泄漏的问题,但是在 threadLocal 的生命周期里(set,getEntry,remove)里,都会针对 key 为 null 的脏 entry 进行处理。

从以上的分析可以看出,使用弱引用的话在 threadLocal 生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。

强引用: 不会被回收的内存。 软引用 : 内部不足的时候回收的内存。 弱引用 : 存活到垃圾回收前的内存。


4. Thread.exit()

当线程退出时会执行 exit 方法:

  1. private void exit() {
  2. if (group != null) {
  3. group.threadTerminated(this);
  4. group = null;
  5. }
  6. /* Aggressively null out all reference fields: see bug 4006245 */
  7. target = null;
  8. /* Speed the release of some of these resources */
  9. threadLocals = null;
  10. inheritableThreadLocals = null;
  11. inheritedAccessControlContext = null;
  12. blocker = null;
  13. uncaughtExceptionHandler = null;
  14. }

从源码可以看出当线程结束时,会令 threadLocals = null,也就意味着 GC 的时候就可以将threadLocalMap 进行垃圾回收,换句话说 threadLocalMap 生命周期实际上 thread 的生命周期相同。

备注: 很多时候,我们都是用在线程池的场景,程序不停止,线程基本不会销毁!!!

由于线程的生命周期很长,如果我们往 ThreadLocal 里面 set 了很大很大的 Object 对象,虽然 set、get 等等方法在特定的条件会调用进行额外的清理,但是 ThreadLocal 被垃圾回收后,在 ThreadLocalMap 里对应的 Entry的键值会变成 null,但是后续在也没有操作 set、get 等方法了。

5. threadLocal最佳实践

通过这篇文章对 threadLocal 的内存泄漏做了很详细的分析,我们可以完全理解 threadLocal 内存泄漏的前因后果,那么实践中我们应该怎么做?

  1. 每次使用完 ThreadLocal,都调用它的 remove() 方法,清除数据。

image.png

  1. 在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。
  2. ThreadLocal 定义为 static

image.png

参考资料