Ref:
https://mp.weixin.qq.com/s/8Ql-5kaUtxiCWyHR6uPPBw
https://www.cnblogs.com/fengzheng/p/8690253.html

1.使用场景

ThreadLocal 归纳下来就 2 类用途:

  • 保存线程上下文信息,在任意需要的地方可以获取。
  • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失。

根据 ThreadLocal 的特性,一些场景:

  • 同一线程在某地方进行设置,在随后的任意地方都可以获取到(复杂业务场景中代替一些参数的显式传递),从而可以用来保存线程上下文信息。
  • 每个请求怎么把一串后续关联起来,就可以用 ThreadLocal 进行 set,在后续的任意需要记录日志的方法里面进行 get 获取到请求 id,从而把整个请求串起来。
  • Spring 的事务管理,用 ThreadLocal 存储 Connection,从而各个 DAO 可以获取同一 Connection,可以进行事务回滚,提交等操作。

    ThreadLocal 的这种用处,很多时候是用在一些优秀的框架里面的,一般我们很少接触,反而下面的场景我们接触的更多一些!

线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!

ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。但是 ThreadLocal 也有局限性,我们来看看阿里规范:
image.png
每个线程往 ThreadLocal 中读写数据是线程隔离,互相之间不会影响的,所以 ThreadLocal 无法解决共享对象的更新问题!

由于不需要共享信息,自然就不存在竞争问题了,从而保证了某些情况下线程的安全,以及避免了某些情况需要考虑线程安全必须同步带来的性能损失!!!

image.png

使用示例

  1. public class ThreadLocalTest {
  2. private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
  3. public static void main(String[] args) {
  4. new Thread(() -> {
  5. try {
  6. for (int i = 0; i < 100; i++) {
  7. // 设值
  8. threadLocal.set(i);
  9. System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());
  10. try {
  11. Thread.sleep(200);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. } finally {
  17. threadLocal.remove();
  18. }
  19. }, "threadLocal1").start();
  20. new Thread(() -> {
  21. try {
  22. for (int i = 0; i < 100; i++) {
  23. System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());
  24. try {
  25. Thread.sleep(200);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. } finally {
  31. threadLocal.remove();
  32. }
  33. }, "threadLocal2").start();
  34. }
  35. }
  36. Output:
  37. ...
  38. threadLocal2====null
  39. threadLocal1====98
  40. threadLocal2====null
  41. threadLocal1====99

2.ThreadLocal 实现

Thread、ThreadLocalMap、ThreadLocal 总览图:
ThreadLocal - 图3
Thread 类有属性变量 threadLocals (类型是 ThreadLocal.ThreadLocalMap),也就是说每个线程有一个自己的 ThreadLocalMap ,所以每个线程往这个 ThreadLocal 中读写是隔离的,互相不会影响。
一个 ThreadLocal 只能存储一个 Object 对象,如果需要存储多个 Object 对象那么就需要多个 ThreadLocal。
如图:
ThreadLocal - 图4
看到上面的几个图,大概思路应该都清晰了,我们 Entry 的 key 指向 ThreadLocal 用虚线表示弱引用 ,下面我们来看看 ThreadLocalMap:

  1. public class ThreadLocal<T> {
  2. public T get() {
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null) {
  6. ThreadLocalMap.Entry e = map.getEntry(this);
  7. if (e != null) {
  8. @SuppressWarnings("unchecked")
  9. T result = (T)e.value;
  10. return result;
  11. }
  12. }
  13. return setInitialValue();
  14. }
  15. public void set(T value) {
  16. Thread t = Thread.currentThread();
  17. ThreadLocalMap map = getMap(t);
  18. if (map != null)
  19. map.set(this, value);
  20. else
  21. createMap(t, value);
  22. }
  23. public void remove() {
  24. ThreadLocalMap m = getMap(Thread.currentThread());
  25. if (m != null)
  26. m.remove(this);
  27. }
  28. ThreadLocalMap getMap(Thread t) {
  29. return t.threadLocals;
  30. }
  31. static class ThreadLocalMap {
  32. private Entry[] table;
  33. /**
  34. * The entries in this hash map extend WeakReference, using
  35. * its main ref field as the key (which is always a
  36. * ThreadLocal object). Note that null keys (i.e. entry.get()
  37. * == null) mean that the key is no longer referenced, so the
  38. * entry can be expunged from table. Such entries are referred to
  39. * as "stale entries" in the code that follows.
  40. */
  41. static class Entry extends WeakReference<ThreadLocal<?>> {
  42. /** The value associated with this ThreadLocal. */
  43. Object value;
  44. Entry(ThreadLocal<?> k, Object v) {
  45. super(k);
  46. value = v;
  47. }
  48. }
  49. private Entry getEntry(ThreadLocal<?> key) {
  50. int i = key.threadLocalHashCode & (table.length - 1);
  51. Entry e = table[i];
  52. if (e != null && e.get() == key)
  53. return e;
  54. else
  55. return getEntryAfterMiss(key, i, e);
  56. }
  57. /**
  58. * Set the value associated with key.
  59. *
  60. * @param key the thread local object
  61. * @param value the value to be set
  62. */
  63. private void set(ThreadLocal<?> key, Object value) {
  64. // We don't use a fast path as with get() because it is at
  65. // least as common to use set() to create new entries as
  66. // it is to replace existing ones, in which case, a fast
  67. // path would fail more often than not.
  68. Entry[] tab = table;
  69. int len = tab.length;
  70. int i = key.threadLocalHashCode & (len-1);
  71. for (Entry e = tab[i];
  72. e != null;
  73. e = tab[i = nextIndex(i, len)]) {
  74. ThreadLocal<?> k = e.get();
  75. if (k == key) {
  76. e.value = value;
  77. return;
  78. }
  79. if (k == null) {
  80. replaceStaleEntry(key, value, i);
  81. return;
  82. }
  83. }
  84. tab[i] = new Entry(key, value);
  85. int sz = ++size;
  86. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  87. rehash();
  88. }
  89. /**
  90. * Expunge a stale entry by rehashing any possibly colliding entries
  91. * lying between staleSlot and the next null slot. This also expunges
  92. * any other stale entries encountered before the trailing null. See
  93. * Knuth, Section 6.4
  94. *
  95. * @param staleSlot index of slot known to have null key
  96. * @return the index of the next null slot after staleSlot
  97. * (all between staleSlot and this slot will have been checked
  98. * for expunging).
  99. */
  100. private int expungeStaleEntry(int staleSlot) {
  101. Entry[] tab = table;
  102. int len = tab.length;
  103. // expunge entry at staleSlot
  104. // 删除无用entry(key弱引用被回收 value尚存)
  105. tab[staleSlot].value = null;
  106. tab[staleSlot] = null;
  107. size--;
  108. // Rehash until we encounter null
  109. Entry e;
  110. int i;
  111. for (i = nextIndex(staleSlot, len);
  112. (e = tab[i]) != null;
  113. i = nextIndex(i, len)) {
  114. ThreadLocal<?> k = e.get();
  115. if (k == null) {
  116. e.value = null;
  117. tab[i] = null;
  118. size--;
  119. } else {
  120. int h = k.threadLocalHashCode & (len - 1);
  121. if (h != i) {
  122. tab[i] = null;
  123. // Unlike Knuth 6.4 Algorithm R, we must scan until
  124. // null because multiple entries could have been stale.
  125. while (tab[h] != null)
  126. h = nextIndex(h, len);
  127. tab[h] = e;
  128. }
  129. }
  130. }
  131. return i;
  132. }
  133. }
  134. }

java 对象的引用包括 : 强引用,软引用,弱引用,虚引用 。
因为这里涉及到弱引用,简单说明下:

  • 弱引用也是用来描述非必需对象的,当 JVM 进行垃圾回收时,无论内存是否充足,该对象仅仅被弱引用关联,那么就会被回收。

当仅仅只有 ThreadLocalMap 中的 Entry 的 key 指向 ThreadLocal 的时候,ThreadLocal 会进行回收的
ThreadLocal 被垃圾回收后,在 ThreadLocalMap 里对应的 Entry 的键值会变成 null,但是 Entry 是强引用,那么 Entry 里面存储的 Object,并没有办法进行回收,所以 ThreadLocalMap 做了一些额外的回收工作。
ThreadLocal - 图5
虽然做了但是也会存在内存泄漏风险(最佳实践使用静态变量)。

3.实践

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

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

  • 应该在我们不使用的时候主动调用 remove 方法进行清理。使用完 ThreadLocal ,手动调用 remove () 方法,例如 Tomcat 的 Session 例子,如果不在拦截器或过滤器中处理,不仅可能出现内存泄漏问题,而且会影响业务逻辑(在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现);
  • 把 ThreadLocal 定义为 static, 由于 ThreadLocal 有强引用在,那么在 ThreadLocalMap 里对应的 Entry 的键会永远存在,那么执行 remove 的时候就可以正确进行定位到并且删除。image.png
  • 最佳实践: ```java public class ContextHolder {

    private static final ThreadLocal CONTEXT_THREAD_LOCAL

    1. = ThreadLocal.withInitial(new Object());
    2. public static void clean() {
    3. CONTEXT_THREAD_LOCAL.remove();

    } }

    try { // 其它业务逻辑 } finally { ContextHolder.clean(); } ```

    其他应用

    Netty 中有 FastThreadLocal, FastThreadLocal 的吞吐量优于 jdkThreadLocal.