很多地方会用到 ThreadLocal 这个工具,最近看到关于它引起内存泄露的问题,于是对 ThreadLocal 的实现以及使用需要注意的问题做了一些调查,记录一下。
ThreadLocal 的实现
实现中几个关键的类和他们之间的引用关系大致上是下面这个图,在下面会说明这些箭头和方块的含义是什么。
java.lang.Thread 内有个 ThreadLocalMap :
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 存储了所有跟该 Thread 绑定的 ThreadLocal 对象到该 ThreadLocal 对象对应 Value 的映射。每次在读取一个 ThreadLocal 对象的值的时候,就是通过 ThreadLocalMap 来查看 ThreadLocal 对象对应的值是什么。因为每个 Thread 都有自己的 ThreadLocalMap,所以同一个 ThreadLocal 对象在不同的 Thread 里能对应不同的 Value。从而实现 ThreadLocal 的功能。
ThreadLocalMap 内是通过数组实现 Map 功能的。数组的元素是 Entry,是个装有 Key Value 的对象。Key 是个弱引用,所以上图用虚线表示,指向一个 ThreadLocal 对象,Value 是强引用指向跟该 ThreadLocal 对象绑定的 Value 值。
碰撞处理
每一个 ThreadLocal 对象都有一个 threadLocalHashCode,在将 ThreadLocal 对象及其对应 Value 放入 ThreadLocalMap 时,先根据 threadLocalHashCode 和 ThreadLocalMap 内数组大小用类似于 threadLocalHashCode % ThreadLocalMap.length() 的方法计算出来该 threadLocalHashCode 对应的哪一个 Slot 的 index,再构造 Entry 放入该 index 指向的 ThreadLocalMap 的 Slot 中。
因为 ThreadLocalMap 内数组大小有限,类似于 threadLocalHashCode % ThreadLocalMap.length() 计算 index 的方法可能出现两个不同的 ThreadLocal 对象带着两个不同的 threadLocalHashCode 但被 Hash 到同一个 Slot 的情况,如下图 ThreadLocalA ThreadLocalB ThreadLocalC 都具有相同的 threadLocalHashCode,在插入 ThreadLocalC 时,根据其 threadLocalHashCode 先被 Hash 到 ThreadLocalA 的 Slot,发现 Slot 不为空,于是 index + 1 再判断临近的已经存了 ThreadLocalB 的 Slot 是否为空,不为空则继续 index + 1 直到找到一个空的 Slot 将 ThreadLocalC 存入。
这种碰撞处理方式也就导致:
- 每个 ThreadLocal 对象的 threadLocalHashCode 不能挨得太近,不然冲突会很多。其计算方法类似于:
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode.getAndAdd(0x61c88647)
0x61c88647 是个很神奇的数字,据说是来自斐波那契散列,但我还没有看懂,所以不细说了。
- ThreadLocalMap 一定不能完全装满,内置的数组一定要比实际存入的 ThreadLocal 对象至少大 1。事实上 ThreadLocalMap 的 Load Factor 是 2/3,超过之后会 rehash 并扩容。不能完全装满的原因是比如还是上面那张图,要获取一个 ThreadLocal 对象对应的 Value,并且这个目标 ThreadLocal 没有存入该 ThreadLocalMap,它的 threadLocalHashCode 刚好也 Hash 到了 ThreadLocalA 对象所在的 Slot,获取的时候先判断目标 ThreadLocal 是不是等于 ThreadLocalA,不是的话再判断是不是等于 ThreadLocalB,依次类推直到获取到一个空 Slot 从而才能知道该 ThreadLocal 没有存储在当前 ThreadLocalMap 中。如果 ThreadLocalMap 完全装满,就不能依赖这个 Slot 是否为空的判断了;
- 清理 ThreadLocalMap 时候要保证将一个 index 指向的 Slot 清理之后,需要连带着将挨着该 index 的非空 Slot 内的 ThreadLocal 对象全部 Rehash 一遍。因为这些 Slot 内存储的 ThreadLocal 对象和 index 指向的 Slot 内存储的 ThreadLocal 对象都 Hash 到了同一个 ThreadLocalMap 内的 Slot,如果把开头 Slot 清理后面的不 Rehash 就无法找到他们了。
ThreadLocalMap 的清理
从上面结构能够看出,Thread 到 ThreadLocalMap 是个强引用,因为 Thread 就是 GC Root,所以 Thread 只要不被清理初始化了的 ThreadLocalMap 一定也不会被清理。并且 ThreadLocalMap 内 Entry 的 Value 也是强引用,于是只要 Thread 存在,在没有别的清理机制存在的情况下 ThreadLocal 的 Value 也是一定不会被清理的。而我们知道 ThreadLocalMap 内 Entry 的 Key 是 WeakReference,它不会阻碍其指向的 ThreadLocal 变量在没有 GC Root 指向时被 GC 掉,被 GC 掉之后,Entry 的 Key 弱引用读取时候会返回 null,而不是之前指向的 ThreadLocal 对象,因为 ThreadLocal 对象已经被 GC 掉。所以我们知道一定是有机制能在 ThreadLocal 对象被 GC 掉之后,将指向该 ThreadLocal 对象的 Entry 从 ThreadLocalMap 中移除。
执行清理 ThreadLocalMap 的操作的有三个地方:
- 主动调用 ThreadLocalMap 内的 remove
- set 值到 ThreadLocalMap 时调用 replaceStaleEntry 和 cleanSomeSlots
- getEntry 时如果发现 Key 找不到会执行 expungeStaleEntry
ThreadLocalMap 的 remove
传入一个 ThreadLocal 对象,并从当前 ThreadLocalMap 中将这个 ThreadLocal 对象的 Entry 清理。会做如下事情:
- 根据目标 ThreadLocal 的 threadLocalHashCode 计算 Hash 的 index;
- 从 index 开始依次遍历 index + 1, index + 2 …. 直到 index + x 指向的 Slot 是空为止,查找 Slot 内 Entry 的 Key 是不是目标 ThreadLocal 对象;
- 如果找到了目标 ThreadLocal 对象,一方面是将 Entry 内 WeakReference 清理,不再指向目标 ThreadLocal 对象;另一方面是将目标 ThreadLocal 对象所在 Slot 通过调用 expungeStaleEntry 清理;
可以想见,expungeStaleEntry 的工作就是传入一个 Slot 的 index,将这个 Slot 内存放的 Entry 清理,并且遍历 index 之后的所有非空 Slot 并 Rehash 这些非空 Slot 中的 ThreadLocal 对象。
ThreadLocalMap 的 set
set 操作是传入一个 ThreadLocal 对象和待和其绑定的 value,将这个 ThreadLocal 和 value 存入 ThreadLocalMap 中。存的时候也是需要先对 ThreadLocal 对象做 Hash 找到其在 ThreadLocalMap 中的 Slot,如果 Slot 被占用,会有三种情况:
- Slot 内存储的 ThreadLocal 对象就是当前待存储的 ThreadLocal 对象,此时只需要用新 Value 替换原来的 Value 就结束了;
- Slot 内存储的 ThreadLocal 不是当前待存储的 ThreadLocal 对象,并且之前存的 ThreadLocal 对象已经被 GC 掉,Slot 内 Entry 的 WeakReference 读取后返回空,这种情况下需要将原来 Entry 废弃并建立新的 Entry 指向这个新的 ThreadLocal 对象,存入当前的 Slot。这个替换过程使用的是 replaceStaleEntry 方法;
- 如果不是上面两种情况,则需要继续查看紧挨着的 Slot 直到遇到空 Slot。找到空 Slot 说明我们找到一个空位置,则创建全新的 Entry 指向当前 ThreadLocal 对象,存入这个找到的空 Slot;
如果是上面第三种情况,添加完新的 Entry 之后,还会执行一次 cleanSomeSlots 方法,它是在当前新添加的 Entry 所在 Slot 之后,连续的找 log N 个 Slot,判断这些 Slot 内存储的 Entry 是否指向一个已经被 GC 的 ThreadLocal 对象,是的话就对这个 Slot 执行 expungeStaleEntry,做清理。其中 N 是当前 ThreadLocalMap 内存储的 ThreadLocal 对象总数。
对于上面第 2 种情况中使用的 replaceStaleEntry 其实现还比较复杂,拿下图来说:
假设当前要存的是 ThreadLocalB,并且 ThreadLocalA B C 在这个 ThreadLocalMap 都具有相同的 Hash 值,从而都 Hash 到同一个 Slot 即现在 ThreadLocalA 所在的 Slot。也正因为碰撞所以 ThreadLocalB C 都是紧挨着 ThreadLocalA 存储的。3 号位 Slot 指向 null 表示它本来是存一个 ThreadLocal 对象的,但这个对象被 GC 了,所以按照上面对 set 方法的描述,再次 set ThreadLocalB 的时候发现 3 号位是 null 就会执行 replaceStaleEntry,希望将 3 号位 replace 为 ThreadLocalB 并绑定上最新的 Value。
但是因为我们只检查到 3 号位,我们只能确认 2 3 两个位置没有 ThreadLocalB 对象,但 ThreadLocalB 对象可能存在于 3 号位之后的 Slot 中所以直接将 ThreadLocalB 存入 3 号位是不行的,需要从 3 号位向后遍历着查找一下看看 3 号位之后还有没有 ThreadLocalB 对象了,如上图所示 3 号位之后还确实是有 ThreadLocalB 对象,并且因为发现 3 号位原来的 ThreadLocal 对象已经被 GC,所以 replaceStaleEntry 需要将 4 号位的 ThreadLocalB 挪到 3 号位,并且将该 ThreadLocalB 对象绑定上新的 Value。交换之后 4 号位我们知道是需要被清理的,所以会调用 expungeStaleEntry 将该位置的 Slot 清理,并且将 4 号位之后的 Slot 都进行 rehash。
在 ThreadLocal 代码中,被 Hash 到同一个 Slot 的所有 ThreadLocal 对象称为一个 run,他们在 ThreadLocalMap 数组内紧挨着存储。当前面 expungeStaleEntry 执行之后,还是会调用 cleanSomeSlots 来探测当前 run 之后,也即 6 号位 Slot 之后 log N 个 Slot 看看有没有被 GC 掉的 ThreadLocal,有的话就用 expungeStaleEntry 做清理。
需要注意的是如果在 4 号位找到 ThreadLocalB,则 4 号位之后是不可能再有 ThreadLocalB 的,所以找到 4 号位做完交换和更新 Value 之后不需要从 4 号位再往后找有没有 ThreadLocalB 了。
除了上面说的这一大堆之外,replaceStaleEntry 实际还会检查同一个 run 内 3 号位之前的 Slot,看看这些 Slot 的 ThreadLocal 对象有没有被 GC 掉,虽然这些 Slot 在 replaceStaleEntry 执行之前,在 set 方法内已经检查过一次。从 replaceStaleEntry 内注释来看主要原因是想避免连续的 rehash。我个人推测,主要是因为 set 操作三种情况中,最耗时的就是第二种需要执行 replaceStaleEntry 的情况,无论是直接找到被更新的 ThreadLocal 对象直接更新绑定的 Value 还是在一个 run 内没有发现被 GC 的 ThreadLocal 对象直接将新的 ThreadLocal 存在一个 run 的末尾的空 Slot 内,耗时都是比较小的,而需要执行 replaceStaleEntry 时因为清理一个 Slot 需要将后续所有 Slot 全部 Rehash 所以耗时最大,所以要尽可能的避免 replaceStaleEntry 的执行。而 GC 是任意时刻都可能执行的,虽然 set 操作内检查过上图 2 号位,但是 GC 过后可能 2 号位的 ThreadLocalA 也被 GC 掉了,所以再次检查一下能更好的避免 replaceStaleEntry 的执行。
如果发现 3 号位之前有 ThreadLocal 对象被 GC,则在替换完 3 号位后,会直接从 3 号位之前这个被 GC 的 ThreadLocal 对象所在 Slot 开始,完整的执行一遍 expungeStaleEntry,全部执行完后相当于是从 expungeStaleEntry 执行开始的 Slot 到一个 run 的末尾所有被 GC 掉的 ThreadLocal 都会被清理。
ThreadLocalMap 的 expungeStaleEntry
说了半天 expungeStaleEntry,最后来看看它。其工作是传入一个 Slot 的 index,将该 index 指向的 Slot 清理,并且将该 index 之后同一个 run 内的所有 Slot 都检查一遍,发现 Slot 指向的 ThreadLocal 被 GC 则也清理该 Slot,没 GC 就将该 ThreadLocal 对象重新 rehash 到 ThreadLocalMap 的其它 Slot 上。最终会返回目标 index 所在 run 的终点序号,也即一个 run 末尾的空 Slot 的 index 值。类似于:
cleanSlot(staleSlot);
Entry e; int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
cleanSlot(i);
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) restoreEntryFromSlotIToSlotH(i, h);
}
}
return i;
其中 table 就是 ThreadLocalMap 内存放 Entry 的数组,len 是数组长度,staleSlot 是传入 expungeStaleEntry 的待清理 Slot 的 index。
能看出来 ThreadLocal 的实现还是挺麻烦的,主要是需要避免 ThreadLocal 对象被 GC 又没有清理 ThreadLocalMap 的情况,以避免下次 set 或者 get 时候导致一堆 Rehash。感觉这也是为什么 ThreadLocal 的使用介绍中都说是让 ThreadLocal 对象声明为 static 的从而跟其所在 Class 绑定,不会被 GC 掉。但这么一来也未内存泄露埋下了隐患,我们接下来看看。
ThreadLocal 自动清理机制会引起内存泄露
- 叫内存泄露好像也不是完全合适,这里就是想记录一下 ThreadLocal 存储的 Value 会出现永久驻留内存的场景。
- 这些场景都是在不主动调用 ThreadLocal 的
remove
方法,完全依赖 ThreadLocal 自身的清理机制下才会出现。如果每次使用 ThreadLocal 对象都记得在使用完后主动调用remove
的话自然是不可能出现内存泄露了。所以这里只记录一下仅仅依靠 ThreadLocal 自动清理机制会出现内存泄露的情况。
不再调用 set
和 getEntry
方法
首先上面 ThreadLocal 实现部分提到过,ThreadLocal 自动清理机制需要依赖于用户调用 ThreadLocalMap 下的 set
和 getEntry
两个方法,如果一个 ThreadLocal 对象已经被 GC,用户不再向同一个 Thread 绑定新的 ThreadLocal 对象,也再不读取 Thread 上的其它 ThreadLocal 对象,就无法触发 ThreadLocalMap 的 set
和 getEntry
方法,导致 ThreadLocal 内存储的 Value 对象永久驻留内存。
这个是因为由于假设 Thread 还存活,Thread 有一条强引用链还能指向这个已被 GC 的 ThreadLocal 对象对应的 Value,造成这个 Value 持续存在于内存,即使你不再使用这个 Value 它还永远存在于内存中。这个强引用链是 Thread -> ThreadLocal.ThreadLocalMap -> ThreadLocal.ThreadLocalMap.Entry -> Value。
因为 ThreadLocal 和 Class 绑定导致 ThreadLocal 无法被 GC
一般 ThreadLocal 在使用时,我们大多会把 ThreadLocal 对象声明为 private static 的,比如 ThreadLocal 文档上提供的例子:
private static final AtomicInteger nextId = new AtomicInteger(0);
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
因为 ThreadLocal 对象被声明为 static 的了,也就是说 ThreadLocal 对象是跟其所在的 Class 绑定的,如果 Class 不被 GC 这个 ThreadLocal 对象就不会被 GC,从而如果 Thread 持续存活时,在该 Thread 上 ThreadLocal 对应的 Value 也不会被 GC,永久的驻留内存。这可能就是你期望的行为,但还是在这么使用 ThreadLocal 时候时刻记得,每个存活的线程都会保留一份 ThreadLocal 对应的 Value,并且会永久驻留内存,如果 Value 比较大,可能会消耗很大一部分内存空间。
Classloader 泄露
最近在这篇文章上看到了个因为 ThreadLocal 对象和 Class 绑定在一起而引起泄露的问题:https://wiki.apache.org/tomcat/MemoryLeakProtection
它的例子如下:
private class MyCounter {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}
public class LeakingServlet extends HttpServlet {
private static ThreadLocal<MyCounter> myThreadLocal = ThreadLocal.withInitial(MyCounter::new);
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
MyCounter counter = myThreadLocal.get();
response.getWriter().println(
"The current thread served this servlet " + counter.getCount()
+ " times");
counter.increment();
}
}
MyCounter 类只被 LeakingServlet 类使用,所以它俩的类加载器会是同一个。
Tomcat 的这种 web 容器,为了应用隔离加载上面 Servlet 和 MyCounter 时会用独立的 WebAppClassLoader 来加载。并且 Tomcat 内线程都是放在线程池里并且永久存在,当加载的 Servlet 内有个跟该 Servlet Class 绑定的 ThreadLocal 对象存在,如上面 myThreadLocal 那样,myThreadLocal 的生命周期就和 LeakingServlet 一样长,Servlet Class 不卸载 myThreadLocal 就一直存在于线程的 ThreadLocalMap 中。而 Servlet Class 要卸载,依赖于加载它的 WebAppClassLoader 卸载。WebAppClassLoader 要卸载依赖于没有指向这个 ClassLoader 的引用,但是线程的 ThreadLocalMap 的 myThreadLocal 对应的 value 是 MyCounter 对象,该对象的 Class 有指向 WebAppClassLoader 的强引用,致使 WebAppClassLoader 无法卸载,最终导致这一连串的东西没有一个能被 GC 掉。
需要注意的是 MyCounter 类的存在非常重要,如果 myThreadLocal 内放的不是 MyCounter 对象,而是别的 ClassLoader 加载的类,比如 AtomicInteger,这样当 LeakingServlet 不再使用的时候就没有 gc Root 上的强引用能指向加载 LeakingServlet 的 WebAppClassLoader 了,这个 ClassLoader 就能被卸载,从而 LeakingServlet 也能被卸载,最终 myThreadLocal 变量能被 GC 再被从线程的 ThreadLocalMap 中移除。这里的强引用链条是 Thread -> ThreadLocalMap -> Value -> myCounter 对象 -> MyCounter 类 -> WebAppClassLoader 加载器。
所以知道使用 ThreadLocal 一个很重要的事情是记得用完之后要 Remove,不要依赖它自己清理的机制。
还有一个能参考的文章在这里:ThreadLocal Memory Leak in Java web application - Tomcat
一些实例
Clojure 的 binding 怎么使用 ThreadLocal 的
clojure 的 binding 在这里 binding - clojure.core | ClojureDocs - Community-Powered Clojure Documentation and Examples
clojure 的 binding 很方便,当我们需要跨函数的传递一些通用的参数时,不一定每个函数都带着这个通用参数,可以将其绑定在 dynamic 变量上,之后同一个线程调用函数时都能使用这个绑定到 dynamic 变量的值。比如:
(def ^:dynamic *dynamic-data-a*)
(def ^:dynamic *dynamic-data-b*)
(binding [*dynamic-data-a* "Hello" *dynamic-data-b* "World"]
(fun-1 some-argument1 some-argument2)
(fun-2 some-argument3 some-argument4))
给 dynamic-data-a 和 dynamic-data-b 绑定了值之后,fun-1 和 fun-2 执行时包括他们调用的其它函数执行时都不用主动传递 dynamic-data-a 和 dynamic-data-b 但是都能直接访问到 dynamic-data-a 和 dynamic-data-b 绑定的值,即分别是 “Hello” 和 “World”,只要这些函数运行在相同的线程上就行。binding 的上述功能就是用 ThreadLocal 实现的。
binding
内最主要的就是将 binding 列表传入 push-thread-bindings 并且通过 finally 在执行完 binding 的 body 之后执行 pop-thread-bindings。
(let []
(push-thread-bindings (hash-map ~@(var-ize bindings)))
(try
~@body
(finally
(pop-thread-bindings))))
clojure 的 Var 内有个 dvals 的 ThreadLocal 变量,指向一个栈,栈内都是当前 Thread 曾经绑定过的 dynamic 的 Var 和其绑定的值如下:
这个图做了简略,没有精确反应 clojure 真实的实现模型,但是原理是一样的。
因为 binding 可以有很多层,可以持续嵌套,比如像这样:
(binding [*dynamic-data-a* "Hello" *dynamic-data-b* "World"]
(fun-1 some-argument1 some-argument2)
(binding [*dynamic-data-c* "Good"]
(fun-2 some-argument3 some-argument4)
(binding [*dynamic-data-d* "Afternoon"]
(fun-3 some-argument5 some-argument6))))
所以上面图中 dvals 指向的 Stack 也能有很多层,每一层都存着所有绑定的 dynamic Var。每一次 binding 都通过 push-thread-bindings 增加一个 Frame 到 Stack。每个 Frame 都有当前 Thread 上所有 binding 的 dynamic 变量。每次访问一个 Var 的值的时候都会判断这个 Var 是不是 dynamic 的,如果是则会使用 deref 的方式访问其值,在 deref 的内会获取当前 Thread dvals 指向的栈,从栈顶 bindings 指向的 Map 中找到被访问 Var 当前绑定的 dynamic 变量值。
clojure 的 Var 类实现中能看到读取这个 Var 值的实现:
final public Object get(){
if(!threadBound.get())
return root;
return deref();
}
final public Object deref(){
TBox b = getThreadBinding();
if(b != null)
return b.val;
return root;
}
public final TBox getThreadBinding(){
if(threadBound.get()) {
IMapEntry e = dvals.get().bindings.entryAt(this);
if(e != null)
return (TBox) e.val();
}
return null;
}
当函数调用完,退出 binding 的时候再通过 pop-thread-bindings 将 Stack 中的 bindings 出栈,从而线程所有 dynamic 变量绑定的值都变成上一层 binding 的值了。
最重要的是,当 bindings 出栈完毕,栈变成空栈时,clojure 会调用 dvals 的 remove
将 ThreadLocal 变量从 Thread 中移除。
Netty 中的 FastThreadLocal
Netty 中有个自己的 ThreadLocal 实现,按注释说法是比原生的 ThreadLocal 要快,那为什么快?在我们了解了原生的 ThreadLocal 实现之后可以来看看,分析一下。
首先是涉及到 FastThreadLocalThread 和这种特殊 Thread 中存储 FastThreadLocal 对象的类,关系如下:
相当于是 FastThreadLocalThread 对 Thread 进行了扩展,新加了个 InternalThreadLocalMap,专门用于存放 “ThreadLocal” 对象,不再使用原生的 Thread 中的 ThreadLocalMap 了。并且这里所谓的 “ThreadLocal” 对象实际指的是 FastThreadLocal 对象。
InternalThreadLocalMap 和 UnpaddedInternalThreadLocalMap 之间的区别就和他们的名字一样,InternalThreadLocalMap 是为了避免 False Sharing 问题做了 padding。这里应该是个很典型的 False Sharing 出现的场景,虽然 InternalThreadLocalMap 是放在 FastThreadLocalThread 里,跟一个 Thread 绑定,而 Thread 又跟一个 CPU 绑定,似乎不同 CPU 都访问自己的 InternalThreadLocalMap 不会出现问题,都是隔离开的没有并发问题性能会很好,但是假若出现比如两个 InternalThreadLocalMap 对象在一个 cache line 里,即使这两个 InternalThreadLocalMap 都绑定到不同的 Thread,在两个不同的 CPU 运行,因为他们都是操作的同一个 cache line,就会出现 cache line 在不同 CPU 之间来回同步的问题,即 Cache bouncing,导致性能下降。这就是 False Sharing 问题。
为啥原生的 Thread 对象内的 ThreadLocalMap 没有 padding?
主要是不管是 InternalThreadLocalMap 还是 ThreadLocalMap 内存放 “ThreadLocal 对象” 对应 Value 的都是个 Object 数组,数组初始化的时候就 new 到 heap 里了,一个在 Heap 中的数组即使你想 Padding 也是做不到的,并且两个对象很难在同一个 cache line 中特别是还有 GC 的存在。所以 InternalThreadLocalMap 内 padding 主要不是为了保护 Object 数组,是为了保护 UnpaddedInternalThreadLocalMap 除了数组之外的其它一些非引用的 primitive type 的值。
为啥一定要 InternalThreadLocalMap 不能在 UnpaddedInternalThreadLocalMap 直接 padding?
主要是因为 Java 内成员布局决定的,想要保护的数据在 UnpaddedInternalThreadLocalMap,padding 又要放在保护数据的后边,将 padding 放到子类中是能保证 padding 一定在 UnpaddedInternalThreadLocalMap 类内成员的后边。
回来继续 FastThreadLocal 这里。FastThreadLocal 对象 set 或 get 值的时候先会获取当前的 Thread 判断它是不是 FastLocalThread,是的话则取 FastLocalThread 中的 InternalThreadLocalMap,之后将当前 FastThreadLocal 对象和 set 的值一起存入 InternalThreadLocalMap 中,或者从 InternalThreadLocalMap 读取当前 FastThreadLocal 对象绑定的值。不是 FastLocalThread 的情况不在这说了。
所以从这里这些内容来看,FastThreadLocal 和原生 ThreadLocal 的区别就在于怎么存储和查找 “ThreadLocal 对象” 绑定的值。与原生的 ThreadLocal 对象不同,InternalThreadLocal 在存储 FastThreadLocal 的时候不会计算 FastThreadLocal 的 Hash 值,也不会处理不同 ThreadLocal 对象 Hash 到相同 Slot 时的碰撞,它内部也是一个 Object 数组,只是为每个 FastThreadLocal 对象都分配了一个固定的并且会随着 FastThreadLocal 对象的分配而全局自增的 index,将 FastThreadLocal 对象存入 InternalThreadLocal 的时候直接使用这个 index 作为序数在 InternalThreadLocalMap 中的 Object 数组中找该 FastThreadLocal 对象对应的 Slot。
因为每个 FastThreadLocal 对象都有自己的 index,并且每分配一个 index 其值就自增一下,最大值是 Integer.MAX 所以 InternalThreadLocal 内的数组最大长度也为 Integer.MAX,FastThreadLocalThread 能缓存的 FastThreadLocal 对象数量也最多是 Integer.MAX 个。因为 FastThreadLocal 对象的存取都不再需要做 Hash,也不用判断 ThreadLocal 对象是否被 GC 而去做各种 rehash,所以其性能理论上是会比原生的 ThreadLocal 要高的,特别是在存取操作特别频繁的时候。
FastThreadLocal 没有用 WeakReference 方式在 FastThreadLocalMap 中存储 FastThreadLocal 对象,也就导致 FastThreadLocal 不会被 GC。所以也就没有了 ThreadLocal 里哪些清理 ThreadLocal 对象的机制了。所以就会要求使用 FastThreadLocal 一定要自己记得在使用完对象之后做清理,忘记清理就一定会出现内存泄露。清理主要是调用 remove 方法,相对于原生的 ThreadLocal 对象来说 FastThreadLocal 对象的 remove 就简单太多了,基本就是按照 index 找到 FastThreadLocalMap 内的数组 index 对应的 Slot 将 Slot 清理。并且清理后也不会缩小 FastThreadLocalMap 内数组大小,也就是说 FastThreadLocalMap 只会不断扩展不会收缩,直到数组扩展到 Integer.MAX 之后抛错:
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
FastThreadLocal 还实现了将 FastThreadLocalThread 内存放的所有 FastThreadLocal 对象全部清理的机制,removeAll 方法。可能跟想象的不一样,removeAll 不是只将 FastThreadLocalMap 内数组直接销毁就完了,而是用一个专门的 index 存了一个 Set 在 FastThreadLocalMap 的数组内,这个 Set 存着所有跟当前 Thread 绑定的 FastThreadLocal 对象,removeAll 时就用这个特殊的 index 从 Set 中读出所有和当前线程绑定的 FastThreadLocal 对象,并执行对象的 remove 方法,将其与 Thread 解绑,全部解绑完毕之后才会完整销毁 FastThreadLocalMap 内的数组。
搞这么麻烦主要是因为每个 FastThreadLocal 对象在被 remove 时候都会调用一个 onRemove 的回调,如果直接清理 FastThreadLocalMap 内的数组这些回调就不可能触发了。这个 onRemove 主要是 PoolThreadCache 在使用,不细说了。
如果原生的 ThreadLocal 对象中能有个 removeAll 就好了,这样 Tomcat 那种 Container 可以考虑每个处理用户请求的线程在处理请求的时候都这么搞一下:
try {
do something
} finally {
FastThreadLocal.removeAll();
}
从而在处理完请求之后能清理 ThreadLocal 对象,并且线程能放回线程池之后再用,也不会引起 ClassLoader 泄露。
所以这么看来,Netty 的实现相对于原生的 ThreadLocal 来说因为省去了每次添加元素时候的 Hash,也省去了每次 set、get 时候可能出现的清理维护 ThreadLocalMap 的工作,所以速度更快一点。但是其使用上要更注意,更小心,set 完一定要记得 remove 或者使用 removeAll,并且 FastThreadLocal 对象数量有上线,不可能分配特别多。
参考
- MemoryLeakProtection - Tomcat Wiki
- http://javarevisited.blogspot.co.at/2013/01/threadlocal-memory-leak-in-java-web.html
- https://veerasundar.com/blog/2010/11/java-thread-local-how-to-use-and-code-sample/
- https://stackoverflow.com/questions/17968803/threadlocal-memory-leak
- https://blog.codecentric.de/2008/09/threadlocal-memoryleak/
- https://neilmadden.wordpress.com/2014/07/21/threadlocal-variables-and-permgen-memory-leaks-not-always-a-leak/