ThreadLocal内存泄漏问题
1.介绍一下ThreadLocalThreadLocal是Java提供的一种线程本地存储机制用户在多线程环境下为每个线程提供独立的变量副本且每个线程之间的变量副本是相互隔离的不会相互影响2.ThreadLocal的基本结构ThreadLocal并不是线程本身的属性而是每一个线程中都维护了一个ThreadLocalMap这个ThreadLocalMapThreadLocalMap中维护了一个Entry类型数组在Entry数组中每一个元素都是一个Entry对象每一个Entry对象中都存储着一个ThreadLocal对象和一个与其对应的value。所以在Thread中可以存储多个ThreadLocal对象也就是说一个线程可以对应多个ThreadLocal变量3.不会出现内存泄漏的情况首先来思考一下什么情况不会内存泄漏如果我们使用Thread类或者Thread类的子类来创建和启动一个线程并且将一个变量存储到ThreadLocal中此时Thread和ThreadLocalMap中就会有一个Thread引用指向ThreadLocalMap的关系如下图如果该线程一直在正常执行任务这个引用关系就会一直存在此时如果这个线程任务执行结束并被退出此时Thread和ThreadLocalMap的引用关系就不存在了如下图线程执行完任务退出后此时Thread内部维护的ThreadLocalMap对象也就失去了强应用此时ThreadLocalMap就会被GC回收掉同时ThreadLocalMap内部的ThreadLocal对象也会被GC回收掉此时就不会出现内存泄漏的问题此时就可以看出一个线程正常执行完任务退出后对应的ThreadLocal对象就会被GC回收从而不会出现内存泄漏的问题4.内存泄漏问题如果在线程池中使用ThreadLocal就会出现内存泄漏问题因为线程池中的核心线程是会被反复循环使用的不会退出此时因为核心线程不会退出Thread和ThreadLocalMap的强引用关系就会一直存在此时ThreadLocalMap就不会被GC回收掉在ThreadLocalMap的内部结构中每一个键值对都是一个Entry对象而在Entry对象中Entry对象的key是ThreadLocal类型的弱引用弱引用是可以被GC自动回收的而Entry对象中的value对象是一个对实际数据强引用强应用是不会被GC自动回收的如果线程池中的核心线程执行任务时通过ThreadLocal.set()方法设置了value但没有及时调用remove方法那么当ThreadLocal对象没有其他强引用指向时ThreadLocal对象就会被GC回收使Entry对象中的key为null但是此时Entry对象中的value是一个强引用是不会被自动回收的。久而久之就会导致很多无用的Entry对象堆积在ThreadLocalMap中因为线程池中的核心线程不会主动退出ThreadLocalMap就不会被回收从而导致value无法被回收导致这些value占用的空间无法被释放从而导致内存泄漏的问题5.如何解决内存泄漏的问题解决内存泄漏的问题我们只要手动调用ThreadLocal中的remov方法就好了在remove方法中会先根据当前线程获取对应的ThreadLocalMap对象接着判断当前ThreadLocalMap对象是否为空如果当前ThreadLocalMap的对象不为空就会调用当前ThreadLocalMap对象的有参remove方法public void remove() { ThreadLocalMap m getMap(Thread.currentThread()); if (m ! null) { // 关键从当前线程的 ThreadLocalMap 中移除当前 ThreadLocal 对应的 Entry // this是指当前ThreadLocal对象 m.remove(this); } }而在这个有参的remove方法中会将对应Entry对象中作为key的ThreadLocal对象置为nullprivate void remove(ThreadLocal? key) { // ThreadLocalMap 内部是一个 Entry 数组类似于 HashMap 的 table Entry[] tab table; int len tab.length; // 计算 key 在数组中的索引位置类似 hash (n-1) int i key.threadLocalHashCode (len - 1); for (Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) { ThreadLocal? k e.get(); // 获取 Entry 的 key是一个弱引用 if (k key) { // 找到了我们要删除的 ThreadLocal 对象 e.clear(); // 清除弱引用key 设置为 null expungeStaleEntry(i); // 清理这个位置并处理可能的后续陈旧 entry关键 return; } if (k null) { // 如果 key 已经是 null被 GC 回收了也要清理这个陈旧的 Entry expungeStaleEntry(i); } } }将Entry对象的key置为null之后有参的remove方法里面又会调用expungeStaleEntry方法expungeStaleEntry方法里面会将key为null的Entry对象的value置为null同时也将该Entry对象置为null此时value被置为null其对应的内存空间就会被GC回收掉了所以通过调用ThreadLocal.remove方法就可以实现对无效Entry对象的value和Entry对象的回收从而避免内存泄漏的问题private int expungeStaleEntry(int staleSlot) { Entry[] tab table; // ThreadLocalMap 内部的 Entry 数组类似 HashMap 的 table int len tab.length; // 1. 清除当前 staleSlot 位置的无效 Entrykey null tab[staleSlot].value null; // 清除 value强引用让 GC 可以回收 tab[staleSlot] null; // 整个 Entry 置为 null彻底移除 size--; // 减少 size 计数 // 2. rehash重新散列 / 清理后续可能受影响的 Entry // 从 staleSlot 的下一个位置开始遍历处理后续的 Entry Entry e; int i; for (i nextIndex(staleSlot, len); (e tab[i]) ! null; i nextIndex(i, len)) { ThreadLocal? k e.get(); if (k null) { // 如果发现当前 Entry 的 key 也是 null已被 GC那么也清理它 e.value null; tab[i] null; size--; } else { // 如果 key 不是 null有效 Entry但它的当前位置可能因为删除导致 hash 冲突 // 需要重新计算它应该存放的正确位置并进行搬迁rehash int h k.threadLocalHashCode (len - 1); if (h ! i) { // 当前位置 i 不是该 Entry 应该在的 hash 位置 tab[i] null; // 先清空当前位置 // 从 h 开始寻找一个空槽位重新插入该 Entry while (tab[h] ! null) h nextIndex(h, len); tab[h] e; } } } return i; // 返回最后一个处理的位置供外部可能继续处理 }6.ThreadLocal有什么问题首先,对于ThreadLocal来说,子线程无法读取到父线程存储的线程副本变量,这也就导致ThreadLocal在线程池的场景下使用会失效,此时TransmittableThreadLocal就很好的解决了上面的两个问题,因为TransmittableThreadLocal是继承于InheritableThreaLocal的,这就让在使用TransmittableThreadLocal时能够父线程的上下文传递给子线程.且TransmittableThreadLocal很好得解决了InheritableThreaLocal在线程池的场景下的上下文污染的问题,由于在线程池中,核心线程是预初始化好的,不断地重复使用预初始化好的线程,此时可能线程池中的一个子线程执行了一个任务,由于没有及时得清除上下文,这就导致当这个子线程去执行下一个任务时,就会带着上次任务的上下文去处理下一个任务,这个就是上下文污染的问题.当一个任务提交到线程池时,TransmittableThreadLocal会介入这个过程.首先提交任务的父线程会调用Capture方法,Capture方法根据父线程中的所有TTL变量生成一个Capture对象,随后这个Capture对象会和提交的任务被封装为一个线程池可执行的任务,当线程池中的工作线程去执行这个封装的任务时,TTL会通过replay方法,通过Capture对象将父线程的上下文放到当前的工作线程中,覆盖工作线程中原有的任何上下文信息,确保工作线程能够在一个正确的上下文环境中执行任务.当任务执行完毕之后,TTL通过restore方法和Capture对象,根据Capture对象中的信息将工作线程中的TTL变量恢复到replay之前的状态,致辞,工作线程就被清理干净了,此时就可以安全的执行下一个任务,避免了上下文污染的问题了,当restore方法执行完毕之后,Capture对象就会被丢弃,等待GC会后.上面这些操作都是TransmittableThreadLocal内部封装好的,不需要我们手动去调用了