阿里二面:谈谈ThreadLocal的内存泄漏问题?问麻了。。。。 ...

打印 上一主题 下一主题

主题 899|帖子 899|积分 2697

弁言

ThreadLocal在Java多线程编程中扮演着重要的角色,它提供了一种线程局部存储机制,答应每个线程拥有独立的变量副本,从而有效地避免了线程间的数据共享冲突。ThreadLocal的主要用途在于,当需要为每个线程维护一个独立的上下文变量时,比如每个线程的事务ID、用户登录信息、数据库连接等,可以减少对同步机制如synchronized关键字或Lock类的依赖,进步体系的执行效率和简化代码逻辑。
但是我们在使用ThreadLocal时,经常因为使用不当导致内存泄漏。此时就需要我们去探究一下ThreadLocal在哪些场景下会出现内存泄漏?哪些场景下不会出现内存泄漏?出现内存泄漏的根本原因又是什么呢?怎样避免内存泄漏?
ThreadLocal原理

ThreadLocal的实现基于每个线程内部维护的一个ThreadLocalMap。
  1. public class Thread implements Runnable {
  2.          /* ThreadLocal values pertaining to this thread. This map is maintained
  3.      * by the ThreadLocal class. */
  4.     ThreadLocal.ThreadLocalMap threadLocals = null;
  5. }
复制代码
ThreadLocalMap是ThreadLocal类的一个静态内部类,ThreadLocal本身不能存储数据,它在作用上更像一个工具类,ThreadLocal类提供了set(T value)、get()等方法来操纵ThreadLocalMap存储数据。
  1. public class ThreadLocal<T> {
  2.     // ...
  3.     public void set(T value) {
  4.         Thread t = Thread.currentThread();
  5.         ThreadLocalMap map = getMap(t);
  6.         if (map != null)
  7.             map.set(this, value);
  8.         else
  9.             createMap(t, value);
  10.     }
  11.     public T get() {
  12.         Thread t = Thread.currentThread();
  13.         ThreadLocalMap map = getMap(t);
  14.         if (map != null) {
  15.             ThreadLocalMap.Entry e = map.getEntry(this);
  16.             if (e != null) {
  17.                 @SuppressWarnings("unchecked")
  18.                 T result = (T)e.value;
  19.                 return result;
  20.             }
  21.         }
  22.         return setInitialValue();
  23.     }
  24.     ThreadLocalMap getMap(Thread t) {
  25.         return t.threadLocals;
  26.     }
  27.     // ...
  28. }
复制代码
而ThreadLocalMap内部维护了一个Entry数据,用来存储数据,Entry继承了WeakReference,所以Entry的key是一个弱引用,可以被GC回收。Entry数组中的每一个元素都是一个Entry对象。每个Entry对象中存储着一个ThreadLocal对象与其对应的value值。
  1. static class ThreadLocalMap {
  2.         static class Entry extends WeakReference<ThreadLocal<?>> {
  3.             /** The value associated with this ThreadLocal. */
  4.             Object value;
  5.             Entry(ThreadLocal<?> k, Object v) {
  6.                 super(k);
  7.                 value = v;
  8.             }
  9.         }
  10. }
复制代码
关于弱引用的知识点,请参考:美团一面:说一说Java中的四种引用类型?
而Entry数组中Entry对象的下标位置是通过ThreadLocal的threadLocalHashCode计算出来的。
  1. private ThreadLocalMap(ThreadLocalMap parentMap) {
  2.         Entry[] parentTable = parentMap.table;
  3.         int len = parentTable.length;
  4.         setThreshold(len);
  5.         table = new Entry[len];
  6.         for (Entry e : parentTable) {
  7.                 if (e != null) {
  8.                         @SuppressWarnings("unchecked")
  9.                         ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
  10.                         if (key != null) {
  11.                                 Object value = key.childValue(e.value);
  12.                                 Entry c = new Entry(key, value);
  13.                                 // 通过key的threadLocalHashCode计算下标,这个key就是ThreadLocall对象
  14.                                 int h = key.threadLocalHashCode & (len - 1);
  15.                                 while (table[h] != null)
  16.                                         h = nextIndex(h, len);
  17.                                 table[h] = c;
  18.                                 size++;
  19.                         }
  20.                 }
  21.         }
  22. }
复制代码
而从Entry数组中获取对应key即ThreadLocal对应的value值时,也是通过key的threadLocalHashCode计算下标,从而可以快速的返回对应的Entry对象。
  1. private Entry getEntry(ThreadLocal<?> key) {
  2. // 通过key的threadLocalHashCode计算下标,这个key就是ThreadLocall对象
  3.         int i = key.threadLocalHashCode & (table.length - 1);
  4.         Entry e = table[i];
  5.         if (e != null && e.get() == key)
  6.                 return e;
  7.         else
  8.                 return getEntryAfterMiss(key, i, e);
  9. }
复制代码
在Thread中,可以存储多个ThreadLocal对象。Thread、ThreadLocal、ThreadLocalMap以及Entry数组的关系如下图:

ThreadLocal在哪些场景下不会出现内存泄漏?

当一个对象失去全部强引用,或者它仅被弱引用、软引用、虚引用关联时,垃圾收集器(GC)通常都能识别并回收这些对象,从而避免内存泄漏的发生。当我们在手动创建线程时,若将变量存储到ThreadLocal中,那么在Thread线程正常运行的过程中,它会维持对内部ThreadLocalMap实例的引用。只要该Thread线程持续执行任务,这种引用关系将持续存在,确保ThreadLocalMap实例及此中存储的变量不会因无引用而被GC回收。

当线程执行完任务并正常退出后,线程与内部ThreadLocalMap实例之间的强引用关系随之断开,这意味着线程不再持有ThreadLocalMap的引用。在这种情况下,失去强引用的ThreadLocalMap对象将符合垃圾收集器(GC)的回收条件,进而被自动回收。与此同时,鉴于ThreadLocalMap内部的键(ThreadLocal对象)是弱引用,一旦ThreadLocalMap被回收,若此时没有其他强引用指向这些ThreadLocal对象,它们也将被GC一并回收。因此,在线程竣事其生命周期后,与之相关的ThreadLocalMap及其包含的ThreadLocal对象理论上都可以大概被准确清理,避免了内存泄漏问题。
现实应用中还需关注ThreadLocalMap中存储的值(非键)是否为强引用类型,因为即便键(ThreadLocal对象)被回收,如果值是强引用且没有其他途径释放,仍可能导致内存泄漏。
ThreadLocal在哪些场景下会出现内存泄漏?

在现实项目开发中,如果为每个任务都手动创建线程,这是一件很泯灭资源的方式,并且在阿里巴巴的开发规范中也提到,不保举使用手动创建线程,保举使用线程池来执行相对应的任务。那么当我们使用线程池时,线程池中的线程跟ThrealLocalMap的引用关系如下:

在使用线程池处理任务时,每一个线程都会关联一个独立的ThreadLocalMap对象,用于存储线程本地变量。由于线程池中的核心线程在完成任务后不会被销毁,而是保持活动状态等待接收新的任务,这意味着核心线程与其内部持有的ThreadLocalMap对象之间始终保持着强引用关系。因此,只要核心线程存活,其所对应的ThreadLocal对象和ThreadLocalMap不会被垃圾收集器(GC)自动回收,此时就会存在内存泄漏的风险。
关于Java中的线程池参数以及原理,请参考:Java线程池最全讲解
出现内存泄漏的根本原因

由上述ThreadLocalMap的结构图以及ThreadLocalMap的源码中,我们知道ThreadLocalMap中包含一个Entry数组,而Entry数组中的每一个元素就是Entry对象,Entry对象中存储的Key就是ThreadLocal对象,而value就是要存储的数据。此中,Entry对象中的Key属于弱引用。
  1. static class ThreadLocalMap {
  2.         static class Entry extends WeakReference<ThreadLocal<?>> {
  3.             /** The value associated with this ThreadLocal. */
  4.             Object value;
  5.             Entry(ThreadLocal<?> k, Object v) {
  6.                 super(k);
  7.                 value = v;
  8.             }
  9.         }
  10. }
复制代码
而对于弱引用WeakReference,在引用的对象使用完毕之后,纵然内存富足,GC也会对其举行回收。
关于弱引用的知识点,请参考:美团一面:说一说Java中的四种引用类型?

当Entry对象中的Key被GC自动回收后,对应的ThreadLocal被GC回收掉了,变成了null,但是ThreadLocal对应的value值依然被Entry引用,不能被GC自动回收。这样就造成了内存泄漏的风险。

在线程池环境下使用ThreadLocal存储数据时,内存泄漏的风险主要源自于线程生命周期管理及ThreadLocalMap内部结构的计划。由于线程池中的核心线程在完成任务后会复用,每个线程都会维持对各自关联的ThreadLocalMap对象的强引用,这确保了只要线程持续存在,其对应的ThreadLocalMap就无法被垃圾收集器(GC)自动回收。
进一步分析,ThreadLocalMap内部采用一个Entry数组来生存键值对,此中每个条目标Key是当前线程中对应ThreadLocal实例的弱引用,这意味着当外部不再持有该ThreadLocal实例的强引用时,Key部分可以大概被GC正常回收。然而,关键在于Entry的Value部分,它直接或间接地持有着强引用的对象,纵然Key因为弱引用特性被回收,但Value所引用的数据却不会随之释放,除非明白移除或者整个ThreadLocalMap随着线程竣事而失效。
所以,在线程池中,如果未准确清理不再使用的ThreadLocal变量,其所持有的强引用数据将在多个任务执行过程中逐渐积累并驻留在线程的ThreadLocalMap中,从而导致潜在的内存泄漏风险。
ThreadLocal怎样避免内存泄漏

颠末上述ThreadLocal原理以及发生内存泄漏的分析,我们知道防止内存泄漏,我们一定要在完成线程内的任务后,调用ThreadLocal的remove()方法来清除当前线程中ThreadLocal所对应的值。其remove方法源码如下:
  1. public void remove() {
  2.          ThreadLocalMap m = getMap(Thread.currentThread());
  3.          if (m != null) {
  4.                  m.remove(this);
  5.          }
  6. }
复制代码
在remove()方法中,起首根据当前线程获取ThreadLocalMap类型的对象,如果不为空,则直接调用该对象的有参remove()方法移除value的值。ThreadLocalMap的remove方法源码如下:
  1. private void remove(ThreadLocal<?> key) {
  2.         Entry[] tab = table;
  3.         int len = tab.length;
  4.         int i = key.threadLocalHashCode & (len-1);
  5.         for (Entry e = tab[i];
  6.                  e != null;
  7.                  e = tab[i = nextIndex(i, len)]) {
  8.                 if (e.get() == key) {
  9.                         e.clear();
  10.                         expungeStaleEntry(i);
  11.                         return;
  12.                 }
  13.         }
  14. }
复制代码
由上述ThreadLocalMap中的set()方法知道ThreadLocal中Entry下标是通过计算ThreadLocal的hashCode得到了,而remove()方法要找到需要移除value所在Entry数组中的下标时,也时通过当前ThreadLocal对象的hashCode获的,然后找到它的下标之后,调用expungeStaleEntry将其value也置为null。我们继承看一下expungeStaleEntry方法的源码:
  1. private int expungeStaleEntry(int staleSlot) {
  2.         Entry[] tab = table;
  3.         int len = tab.length;
  4.         // expunge entry at staleSlot
  5.         tab[staleSlot].value = null;
  6.         tab[staleSlot] = null;
  7.         size--;
  8.         // Rehash until we encounter null
  9.         Entry e;
  10.         int i;
  11.         for (i = nextIndex(staleSlot, len);
  12.                  (e = tab[i]) != null;
  13.                  i = nextIndex(i, len)) {
  14.                 ThreadLocal<?> k = e.get();
  15.                 if (k == null) {
  16.                         e.value = null;
  17.                         tab[i] = null;
  18.                         size--;
  19.                 } else {
  20.                         int h = k.threadLocalHashCode & (len - 1);
  21.                         if (h != i) {
  22.                                 tab[i] = null;
  23.                                 // Unlike Knuth 6.4 Algorithm R, we must scan until
  24.                                 // null because multiple entries could have been stale.
  25.                                 while (tab[h] != null)
  26.                                         h = nextIndex(h, len);
  27.                                 tab[h] = e;
  28.                         }
  29.                 }
  30.         }
  31.         return i;
  32. }
复制代码
在expungeStaleEntry()方法中,会将ThreadLocal为null对应的value设置为null,同时会把对应的Entry对象也设置为null,并且会将全部ThreadLocal对应的value为null的Entry对象设置为null,这样就去除了强引用,便于后续的GC举行自动垃圾回收,也就避免了内存泄漏的问题。即调用完remove方法之后,ThreadLocalMap的结构图如下:

在ThreadLocal中,不仅仅是remove()方法会调用expungeStaleEntry()方法,在set()方法和get()方法中也可能会调用expungeStaleEntry()方法来清理数据。这种计划确保了纵然没有显式调用remove()方法,体系也会在必要时自动清理不再使用的ThreadLocal变量占用的内存资源。
需要我们特别注意的是,尽管ThreadLocal提供了remove这种机制来防止内存泄漏,但它并不会自动执行相关的清理操纵。所以为了确保资源有效释放并避免潜在的内存泄漏问题,我们应当在完成对ThreadLocal对象中数据的使用后,及时调用其remove()方法。我们最好(也是必须)是在try-finally代码块结构中,在finally块中明白地执行remove()方法,这样纵然在处理过程中抛出异常,也能确保ThreadLocal关联的数据被清除,从而有利于GC回收不再使用的内存空间,避免内存泄漏。
总结

本文探讨了ThreadLocal的工作原理以及其内存泄漏问题及解决计谋。ThreadLocal通过为每个线程提供独立的变量副本,实现多线程环境下的数据隔离。其内部通过ThreadLocalMap与当前线程绑定,使用弱引用管理键值对。但是,如果未及时清理不再使用的ThreadLocal变量,可能导致内存泄漏,尤其是在线程池场景下。解决办法包括在完成任务后调用remove方法移除无用数据。准确理解和使用ThreadLocal可以大概有效提升并发编程效率,但务必关注潜在的内存泄漏风险。
本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构计划、口试题、步伐员攻略等

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

吴旭华

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表