应用场景:

  1. 保存每个线程独享的对象
  2. 每个线程内需要独立保存信息

  3. Thread类中有一个ThreadLocal.ThreadLocalMap threadLocals = null的变量,这个ThreadLocal相当于是Thread类和ThreadLocalMap的桥梁,在ThreadLocal中有静态内部类ThreadLocalMapThreadLocalMap中有Entry数组。

  4. 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为 key,值为 value 的Entry往这个threadLocalMap中存放。
  5. t.threadLocals = new ThreadLocalMap(this, firstValue)如下这行代码,可以知道每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本。

    remove方法能防止内存泄漏问题。

ThreadLocal中有一个ThreadLocalMap,map结构,其中key为ThreadLocal,value为存储的值。

Entry的 Key 即ThreadLocal对象是采用弱引用引入的,如源代码:

  1. static class ThreadLocalMap {
  2. /**
  3. * The entries in this hash map extend WeakReference, using
  4. * its main ref field as the key (which is always a
  5. * ThreadLocal object). Note that null keys (i.e. entry.get()
  6. * == null) mean that the key is no longer referenced, so the
  7. * entry can be expunged from table. Such entries are referred to
  8. * as "stale entries" in the code that follows.
  9. */
  10. static class Entry extends WeakReference<ThreadLocal<?>> {
  11. /** The value associated with this ThreadLocal. */
  12. Object value;
  13. Entry(ThreadLocal<?> k, Object v) {
  14. super(k);
  15. value = v;
  16. }
  17. }
  1. 为什么ThreadLocalMap使用弱引用存储ThreadLocal

假如使用强引用,当ThreadLocal不再使用需要回收时,发现某个线程中ThreadLocalMap存在该ThreadLocal的强引用,无法回收,造成内存泄漏。
因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。

  1. 那通常说的ThreadLocal内存泄漏是如何引起的呢?

我们注意到Entry对象中,虽然 Key(ThreadLocal)是通过弱引用引入的,但是 value 即变量值本身是通过强引用引入。
这就导致,假如不作任何处理,由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但 value 还是驻留在线程的ThreadLocalMapEntry中。即存在 key 为 null,但 value 却有值的无效Entry。导致内存泄漏。
但实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作。
即如下方法:

  1. /**
  2. * Expunge a stale entry by rehashing any possibly colliding entries
  3. * lying between staleSlot and the next null slot. This also expunges
  4. * any other stale entries encountered before the trailing null. See
  5. * Knuth, Section 6.4
  6. *
  7. * @param staleSlot index of slot known to have null key
  8. * @return the index of the next null slot after staleSlot
  9. * (all between staleSlot and this slot will have been checked
  10. * for expunging).
  11. */
  12. private int expungeStaleEntry(int staleSlot) {
  13. Entry[] tab = table;
  14. int len = tab.length;
  15. // expunge entry at staleSlot
  16. tab[staleSlot].value = null;
  17. tab[staleSlot] = null;
  18. size--;
  19. // Rehash until we encounter null
  20. Entry e;
  21. int i;
  22. for (i = nextIndex(staleSlot, len);
  23. (e = tab[i]) != null;
  24. i = nextIndex(i, len)) {
  25. ThreadLocal<?> k = e.get();
  26. if (k == null) {
  27. e.value = null;
  28. tab[i] = null;
  29. size--;
  30. } else {
  31. int h = k.threadLocalHashCode & (len - 1);
  32. if (h != i) {
  33. tab[i] = null;
  34. // Unlike Knuth 6.4 Algorithm R, we must scan until
  35. // null because multiple entries could have been stale.
  36. while (tab[h] != null)
  37. h = nextIndex(h, len);
  38. tab[h] = e;
  39. }
  40. }
  41. }
  42. return i;
  43. }

上述方法的作用是擦除某个下标的Entry(置为 null,可以回收),同时检测整个Entry[]表中对 key 为 null 的Entry一并擦除,重新调整索引。
该方法,在每次调用ThreadLocalgetsetremove方法时都会执行,即ThreadLocal内部已经帮我们做了对 key 为 null 的Entry的清理工作。
但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。

总结

  • (强制)在代码逻辑中使用完ThreadLocal,都要调用remove方法,及时清理。

目前我们使用多线程都是通过线程池管理的,对于核心线程数之内的线程都是长期驻留池内的。显式调用remove一方面是防止内存泄漏,最为重要的是,不及时清除有可能导致严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)。
最佳实践:在**ThreadLocal**使用前后都调用**remove**清理,同时对异常情况也要在**finally**中清理。

  • (非规范)对ThreadLocal是否使用全局static修饰的讨论。

在某些代码规范中遇到过这样一条要求:“尽量不要使用全局的ThreadLocal”。关于这点有两种解读。最初我的解读是,因为静态变量的生命周期和类的生命周期是一致的,而类的卸载时机可以说比较苛刻,这会导致静态ThreadLocal无法被垃圾回收,容易出现内存泄漏。另一个解读,我咨询了编写该规范的对方解释是,如果流程中改变了变量值,下次复用该流程可能导致获取到非预期的值。
但实际上,这两个解读都是不必要的,首先,静态ThreadLocal资源回收的问题,即使ThreadLocal本身无法回收,但线程中的Entry是可以通过remove清理掉的也就不会出现泄漏。第二种解读,多次复用值改变的问题,其实在调用remove后也不会出现。
而如果ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal创建。这样即使线程在访问时不出现错误也有资源浪费。
因此,**ThreadLocal**一般加**static**修饰,同时要遵循第一条及时清理。