1. ThreadLocal 简介

synchronzed 或者 lock 是通过控制线程对临界区资源的同步顺序从而解决线程安全的问题。但是这种加锁会让未获取到锁的线程进行阻塞等待,时间效率不好。

线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,那么,如果每个线程都使用自己的「共享资源」,各自使用各自的,又互相不影响,即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。这也是一种空间换时间的方案。

  • 缺点是:内存会大很多;
  • 优点是:不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。

打个比方说,现在有 100 个同学需要填写一张表格但是只有一支笔,同步就相当于A使用完这支笔后给B,B使用后给C用……老师就控制着这支笔的使用顺序,使得同学之间不会产生冲突。 而threadLocal就相当于,老师直接准备了100支笔,这样每个同学都使用自己的,同学之间就不会产生冲突。


虽然 ThreadLocal 并不在 java.util.concurrent包中而在java.lang包中,但我更倾向于把它当作是一种并发容器(虽然真正存放数据的是 ThreadLoclMap)进行归类。从 ThreadLocal 这个类名可以顾名思义的进行理解,表示线程的「本地变量」,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争

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

总结

  • 线程并发:在多线程并发场景下使用
  • 传递数据:可以通过 ThreadLocal 在同一线程,不同组件间传递公共变量(在有需要的地方可以直接获取,避免参数传递带来的代码耦合问题)
  • 线程隔离:每个线程的变量都是独立的,不会相互影响 |
    | synchronized | ThreadLocal | | —- | —- | —- | | 原理 | 同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问 | 采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 | | 侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |

2. ThreadLocal 的内部结构

2.1 常见的误解

如果我们不去看源代码的话,可能会猜测 ThreadLocal 是这样子设计的:每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK 最早期的 ThreadLocal 确实是这样设计的,但现在早已不是了。
image.png

2.2 现在的设计

JDK 后面优化了设计方案,在 JDK8 中 ThreadLocal 的设计是:每个 **Thread** 维护一个 **ThreadLocalMap**,这个 Map 的 **key****ThreadLocal** 实例本身,**value** 才是真正要存储的值 **Object**

具体的过程是这样的:

  • 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)
  • Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value)
  • Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

image.png

2.3 这样设计的好处

这个设计与我们一开始说的设计刚好相反,这样设计有如下两个优势:

  • 这样设计之后每个 **Map** 存储的 **Entry** 数量就会变少。因为之前的存储数量由 **Thread** 的数量决定,现在是由 **ThreadLocal** 的数量决定。在实际运用当中,往往 **ThreadLocal** 的数量要少于 **Thread** 的数量。
  • **Thread** 销毁之后,对应的 **ThreadLocalMap** 也会随之销毁,能减少内存的使用。

    3. ThreadLocal 的实现原理

    除了构造方法之外, ThreadLocal 对外暴露的方法有以下4个
    image.png

    3.1 set 方法

    **set** 方法设置在当前线程中 threadLocal 变量的值,该方法的源码为:
  1. public void set(T value) {
  2. //1. 获取当前线程实例对象
  3. Thread t = Thread.currentThread();
  4. //2. 通过当前线程实例获取到 ThreadLocalMap 对象
  5. ThreadLocalMap map = getMap(t);
  6. if (map != null)
  7. //3. 如果 Map 不为 null,则以当前 threadLocl 实例为 key,值为 value 进行存入
  8. map.set(this, value);
  9. else
  10. //4.map 为null,则新建 ThreadLocalMap 并存入 value
  11. createMap(t, value);
  12. }

数据 value 是真正的存放在了 ThreadLocalMap 这个容器中了,并且是以当前 threadLocal 实例为 key

首先 ThreadLocalMap 是怎样来的?源码很清楚,是通过getMap(t)进行获取:

  1. ThreadLocalMap getMap(Thread t) {
  2. return t.threadLocals;
  3. }

该方法直接返回的就是当前线程对象 t 的一个成员变量 threadLocals:

  1. /* ThreadLocal values pertaining to this thread. This map is maintained
  2. * by the ThreadLocal class. */
  3. ThreadLocal.ThreadLocalMap threadLocals = null;

也就是说 ThreadLocalMap 的引用是作为 Thread 的一个成员变量,被 Thread 进行维护的。回过头再来看看 set 方法,当 map 为 Null 的时候会通过 createMap(t,value)方法:

  1. void createMap(Thread t, T firstValue) {
  2. t.threadLocals = new ThreadLocalMap(this, firstValue);
  3. }

该方法就是 **new** 一个 ThreadLocalMap 实例对象,然后同样以当前 threadLocal 实例作为 key,值为 value 存放到 threadLocalMap 中,然后将当前线程对象的 threadLocals 赋值为 threadLocalMap

现在来对 set 方法进行总结一下:

  • 通过当前线程对象 thread 获取该 thread 所维护的 threadLocalMap
  • 若 threadLocalMap 不为 null,则以 threadLocal 实例为 key,值为 value 的键值对存入 threadLocalMap
  • 若 threadLocalMap 为 null 的话,就新建 threadLocalMap 然后在以 threadLocal 为键,值为 value 的键值对存入即可。

2.2 get 方法

get 方法是获取当前线程中 threadLocal 变量的值,同样的还是来看看源码:

  1. /**
  2. * 返回当前线程中保存ThreadLocal的值
  3. * 如果当前线程没有此ThreadLocal变量,
  4. * 则它会通过调用{@link #initialValue} 方法进行初始化值
  5. *
  6. * @return 返回当前线程对应此ThreadLocal的值
  7. */
  8. public T get() {
  9. //1. 获取当前线程的实例对象
  10. Thread t = Thread.currentThread();
  11. //2. 获取当前线程的threadLocalMap
  12. ThreadLocalMap map = getMap(t);
  13. if (map != null) {
  14. //3. 获取map中当前threadLocal实例为key的值的entry
  15. ThreadLocalMap.Entry e = map.getEntry(this);
  16. if (e != null) {
  17. @SuppressWarnings("unchecked")
  18. //4. 当前entitiy不为null的话,就返回相应的值value
  19. T result = (T)e.value;
  20. return result;
  21. }
  22. }
  23. //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
  24. return setInitialValue();
  25. }

看下 setInitialValue 主要做了些什么事情?

  1. private T setInitialValue() {
  2. T value = initialValue();
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null)
  6. map.set(this, value);
  7. else
  8. createMap(t, value);
  9. return value;
  10. }

这段方法的逻辑和 set 方法几乎一致,另外值得关注的是 initialValue 方法:

  1. protected T initialValue() {
  2. return null;
  3. }

这个方法是 **protected** 修饰的也就是说继承 **ThreadLocal** 的子类可重写该方法,实现赋值为其他的初始值

关于 get 方法来总结一下:

  • 通过当前线程 **thread** 实例获取到它所维护的 **threadLocalMap**
  • 然后以当前 **threadLocal** 实例为 **key** 获取该 **map** 中的键值对(Entry)
  • **Entry** 不为 **null** 则返回 **Entry****value**
  • 如果获取 **threadLocalMap****null** 或者 **Entry****null** 的话,就以当前 **threadLocal****Key****value****null** 存入 **map** 后,并返回 null。

proteced 修饰的方法,用于同包调用或者不同包的子类重写覆盖用

2.3 remove 方法

  1. public void remove() {
  2. //1. 获取当前线程的threadLocalMap
  3. ThreadLocalMap m = getMap(Thread.currentThread());
  4. if (m != null)
  5. //2. 从map中删除以当前threadLocal实例为key的键值对
  6. m.remove(this);
  7. }

get,set 方法实现了存数据和读数据,我们当然还得学会如何删数据。删除数据当然是从 map 中删除数据,先获取与当前线程相关联的 threadLocalMap 然后从 map 中删除该 threadLocal 实例为 key 的键值对 Entry 即可。

4. ThreadLocalMap 详解

从上面的分析我们已经知道,数据其实都放在了threadLocalMap 中,threadLocal 的 getsetremove 方法实际上具体是通过 threadLocalMap 的 getEntrysetremove 方法实现的。

ThreadLocalMapThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也是独立实现
image.png

4.1 ThreadLocalMap 结构

  1. /**
  2. * 初始容量 —— 必须是2的整次幂
  3. */
  4. private static final int INITIAL_CAPACITY = 16;
  5. /**
  6. * 存放数据的table,Entry类的定义在下面分析
  7. * 同样,数组长度必须是2的整次幂。
  8. */
  9. private Entry[] table;
  10. /**
  11. * 数组里面 entrys 的个数,可以用于判断table当前使用量是否超过阈值。
  12. */
  13. private int size = 0;
  14. /**
  15. * 进行扩容的阈值,表使用量大于它的时候进行扩容。
  16. */
  17. private int threshold; // Default to 0

通过注释可以看出,table 数组的长度为 2 的幂次方。接下来看下 Entry 是什么:

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }

Entry 是一个以 ThreadLocal 为 key,Object 为 value 的键值对,另外需要注意的是这里的 threadLocal 是弱引用,因为 Entry 继承了 **WeakReference**,在 Entry 的构造方法中,调用了 **super(k)** 方法就会将 threadLocal 实例包装成一个 **WeakReferenece**到这里我们可以用一个图来理解下thread, threadLocal, threadLocalMap, Entry 之间的关系:

image.png
image.png
Thread 类有属性变量 threadLocals (类型是 ThreadLocal.ThreadLocalMap),也就是说每个线程有一个自己的 ThreadLocalMap ,所以每个线程往这个 ThreadLocal 中读写隔离的,并且是互相不会影响的。一个 ThreadLocal 只能存储一个 Object 对象,如果需要存储多个 Object 对象那么就需要多个 ThreadLocal.

注意上图中的实线表示强引用,虚线表示弱引用。

  • 如图所示,每个线程实例中可以通过 threadLocals 获取到 threadLocalMap,而 threadLocalMap 实际上就是一个以 threadLocal 实例为 key,任意对象为 value 的 Entry 数组。
  • 当我们为 threadLocal 变量赋值,实际上就是以当前 threadLocal 实例为 key,值为 value 的 Entry 往这个 threadLocalMap 中存放。
  • 需要注意的是 Entry 中的 key 是弱引用,当 threadLocal 外部强引用(threadLocalRef)被置为 null (**threadLocalInstance=null**),那么系统 GC 的时候,根据可达性分析,这个 threadLocal 实例就没有任何一条链路能够引用到它,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:**Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value** 永远无法回收,造成内存泄漏。
  • 当然,如果当前 thread 运行结束,threadLocal,threadLocalMap,Entry 没有引用链可达,在垃圾回收的时候都会被系统进行回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的,所以,threadLocal 的内存泄漏问题,是应该值得我们思考和注意的问题,关于这个问题可以看这篇文章——详解threadLocal内存泄漏问题

    4.2 弱引用和内存泄露

    内存泄漏相关概念

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。

  • Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出

弱引用相关概念

  • 强引用(Strong Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
  • 弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存(GC 时回收)。
  • 软引用(SoftReference),内存不够时进行回收

    4.2.1 如果 key 使用强引用

    image.png
    假设在业务代码中使用完 ThreadLocalthreadLocal Ref 被回收了。但是因为 threadLocalMapEntry 强引用了 threadLocal,造成 threadLocal 无法被回收。

在没有手动删除这个 **Entry** 以及 **CurrentThread** 依然运行的前提下,始终有强引用链 **threadRef->currentThread->threadLocalMap->entry**,Entry 就不会被回收(Entry中包括了 ThreadLocal 实例和 value),导致 Entry 内存泄漏。

4.2.2 如果 key 使用弱引用

同样假设在业务代码中使用完 ThreadLocal ,threadLocal Ref 被回收了。由于 ThreadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal 实例, 所以 threadlocal 就可以顺利被gc回收,此时 Entry 中的 key=null

但是在没有手动删除这个 **Entry** 以及 **CurrentThread** 依然运行的前提下,也存在有强引用链 **threadRef->currentThread->threadLocalMap->entry -> value** ,value 不会被回收, 而这块 value 永远不会被访问到了,导致 value 内存泄漏

4.2.3 出现内存泄漏的真实原因

内存泄漏的发生跟 ThreadLocalMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢? :::info

  1. 没有手动删除这个 Entry
  2. CurrentThread 依然运行 :::
  • 第一点很好理解,只要在使用完 ThreadLocal,调用其 **remove** 方法删除对应的 Entry,就能避免内存泄漏
  • 关于第二点:由于 **ThreadLocalMap****Thread** 的一个属性,被当前线程所引用,所以它的生命周期跟 **Thread** 一样长。那么在使用完 **ThreadLocal** 之后,如果当前 **Thread** 也随之执行结束,**ThreadLocalMap** 自然也会被 gc 回收,从根源上避免了内存泄漏。

综上:ThreadLocal 内存泄漏的根源是:由于 **ThreadLocalMap** 的生命周期跟 **Thread** 一样长,如果没有手动删除对应 **key** 就会导致内存泄漏。

4.2.4 已经做的改进

1. 使用弱引用

只要记得在使用完 ThreadLocal 及时的调用 remove,无论 key 是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?

事实上,在 **ThreadLocalMap** 中的 **set/getEntry** 方法中,会对 key 为 null(也即是 ThreadLocal 为 null)进行判断,如果为 null 的话,那么是会对 value 置为 null 的。

这就意味着使用完 ThreadLocalCurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 **set,get,remove** 中的任一方法的时候会被清除,从而避免内存泄漏

从以上 setgetEntryremove方法看出,**threadLocal** 的生命周期里,针对 threadLocal 存在的内存泄漏的问题,都会通过 **expungeStaleEntry****cleanSomeSlots****replaceStaleEntry**这三个方法清理掉 key 为 null 的脏 entry
image.png

具体源码分析:从源码深入详解ThreadLocal内存泄漏问题

2. Thread.exit() 线程退出时

当线程退出时会执行 exit 方法:

  1. private void exit() {
  2. if (group != null) {
  3. group.threadTerminated(this);
  4. group = null;
  5. }
  6. /* Aggressively null out all reference fields: see bug 4006245 */
  7. target = null;
  8. /* Speed the release of some of these resources */
  9. threadLocals = null;
  10. inheritableThreadLocals = null;
  11. inheritedAccessControlContext = null;
  12. blocker = null;
  13. uncaughtExceptionHandler = null;
  14. }

从源码可以看出当线程结束时,会令 threadLocals = null,也就意味着 GC 的时候就可以将 threadLocalMap 进行垃圾回收,换句话说 **threadLocalMap** 生命周期实际上 **thread** 的生命周期相同。

备注:很多时候,我们都是用在线程池的场景,程序不停止,线程基本不会销毁!!! ThreadLocal 阿里规范: image.png

4.3 hash冲突的解决

4.3.1 构造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

  1. /*
  2. * firstKey : 本ThreadLocal实例(this)
  3. * firstValue : 要保存的线程本地变量
  4. */
  5. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  6. //初始化table
  7. table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
  8. //计算索引(重点代码)
  9. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  10. //设置值
  11. table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
  12. size = 1;
  13. //设置阈值
  14. setThreshold(INITIAL_CAPACITY);
  15. }

构造函数首先创建一个长度为 16的 Entry 数组,然后计算出 firstKey 对应的索引,然后存储到 table 中,并设置 size 和threshold。

重点分析int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

关于 **firstKey.threadLocalHashCode**

这里定义了一个 AtomicInteger 类型,每次获取当前值并加上 HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在 2 的 n 次方的数组里, 也就是 Entry[] table 中,这样做可以尽量避免 hash 冲突。

关于 **& (INITIAL_CAPACITY - 1)**

计算 hash 的时候里面采用了 hashCode & (size - 1) 的算法,这相当于取模运算 hashCode % size 的一个更高效的实现。正是因为这种算法,我们要求 size 必须是2的整次幂,这也能保证在索引不越界的前提下,使得 hash 发生冲突的次数减小。

4.3.2 ThreadLocalMap 中的 set 方法

  1. private void set(ThreadLocal<?> key, Object value) {
  2. ThreadLocal.ThreadLocalMap.Entry[] tab = table;
  3. int len = tab.length;
  4. //计算索引(重点代码,刚才分析过了)
  5. int i = key.threadLocalHashCode & (len-1);
  6. /**
  7. * 使用线性探测法查找元素(重点代码)
  8. */
  9. for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
  10. e != null;
  11. e = tab[i = nextIndex(i, len)]) {
  12. ThreadLocal<?> k = e.get();
  13. //ThreadLocal 对应的 key 存在,直接覆盖之前的值
  14. if (k == key) {
  15. e.value = value;
  16. return;
  17. }
  18. // key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,
  19. // 当前数组中的 Entry 是一个陈旧(stale)的元素
  20. if (k == null) {
  21. //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
  22. replaceStaleEntry(key, value, i);
  23. return;
  24. }
  25. }
  26. //ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
  27. tab[i] = new Entry(key, value);
  28. int sz = ++size;
  29. /**
  30. * cleanSomeSlots用于清除那些e.get()==null的元素,
  31. * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
  32. * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行 * rehash(执行一次全表的扫描清理工作)
  33. */
  34. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  35. rehash();
  36. }
  37. /**
  38. * 获取环形数组的下一个索引
  39. */
  40. private static int nextIndex(int i, int len) {
  41. return ((i + 1 < len) ? i + 1 : 0);
  42. }

代码执行流程:

  • 首先还是根据 key 计算出索引 i,然后查找 i 位置上的 Entry,
  • 若是 Entry 已经存在并且 key 等于传入的 key,那么这时候直接给这个 Entry 赋新的 value 值,
  • 若是 Entry 存在,但是 key 为 null,则调用 replaceStaleEntry 来更换这个 key 为空的 Entry,
  • 不断循环检测,直到遇到为 null 的地方,这时候要是还没在循环过程中 return,那么就在这个 null 的位置新建一个 Entry,并且插入,同时 size 增加 1。
  • 最后调用 cleanSomeSlots,清理 key 为 null 的 Entry,最后返回是否清理了 Entry,接下来再判断 sz 是否 >= thresgold 达到了 rehash 的条件,达到的话就会调用 rehash 函数执行一次全表的扫描清理。

重点分析 : ThreadLocalMap 使用**线性探测法**来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

举个例子,假设当前 table 长度为16,也就是说如果计算出来 key 的 hash 值为14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,这个时候如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入。

按照上面的描述,可以把 Entry[] table 看成一个环形数组。

6. ThreadLocal 的使用场景

ThreadLocal 不是用来解决共享对象的多线程访问问题的,数据实质上是放在每个 thread 实例引用的 threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响。因此 threadLocal 只适用于 共享对象会造成线程安全 的业务场景。

比如 hibernate 中通过 threadLocal 管理 Session 就是一个典型的案例,不同的请求线程(用户)拥有自己的 session,若将 session 共享出去被多线程访问,必然会带来线程安全问题。

6.1 SimpleDateFormat 问题

下面,我们自己来写一个例子,SimpleDateFormat.parse 方法会有线程安全的问题,我们可以尝试使用 threadLocal 包装 SimpleDateFormat,将该实例不被多线程共享即可。

  1. public class ThreadLocalDemo {
  2. private static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<>();
  3. public static void main(String[] args) {
  4. ExecutorService executorService = Executors.newFixedThreadPool(10);
  5. for (int i = 0; i < 100; i++) {
  6. executorService.submit(new DateUtil("2019-11-25 09:00:" + i % 60));
  7. }
  8. }
  9. static class DateUtil implements Runnable {
  10. private String date;
  11. public DateUtil(String date) {
  12. this.date = date;
  13. }
  14. @Override
  15. public void run() {
  16. if (sdf.get() == null) {
  17. sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
  18. } else {
  19. try {
  20. Date date = sdf.get().parse(this.date);
  21. System.out.println(date);
  22. } catch (ParseException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. }
  27. }
  28. }
  • 如果当前线程不持有 SimpleDateformat 对象实例,那么就新建一个并把它设置到当前线程中,如果已经持有,就直接使用。另外,**if (sdf.get() == null){....}else{.....}**可以看出为每一个线程分配一个 SimpleDateformat 对象实例是从应用层面(业务代码逻辑)去保证的。
  • 在上面我们说过 threadLocal 有可能存在内存泄漏,在使用完之后,最好使用 remove 方法将这个变量移除,就像在使用数据库连接一样,及时关闭连接。

6.2 工作中关于用 ThreadLocal 管理用户 Session 的实例

  1. public class UserProvider {
  2. private static final TransmittableThreadLocal<UserVo> SESSION_USERS = new TransmittableThreadLocal<>();
  3. public static UserVo getSessionUser() {
  4. return SESSION_USERS.get();
  5. }
  6. public static void setSessionUser(UserVo user) {
  7. SESSION_USERS.set(user);
  8. }
  9. public static void cleanSessionUser() {
  10. SESSION_USERS.remove();
  11. }
  12. }

7. 阿里巴巴-开源项目 / transmittable-thread-local

使用如上边的代码。

🔧 功能

👉 在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。 一个Java标准库本应为框架/中间件设施开发提供的标配能力,本库功能聚焦 & 0依赖,支持Java 17/16/15/14/13/12/11/10/9/8/7/6。

JDK[InheritableThreadLocal](https://docs.oracle.com/javase/10/docs/api/java/lang/InheritableThreadLocal.html)类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

🎨 需求场景

ThreadLocal的需求场景即是TransmittableThreadLocal的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal』则是TransmittableThreadLocal目标场景。
下面是几个典型场景例子。

  1. 分布式跟踪系统 或 全链路压测(即链路打标)
  2. 日志收集记录系统上下文
  3. SessionCache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

    8. 其他文章