1、类图结构

只要线程处于活动状态并且可以访问 ThreadLocal 实例,每个线程都持有对其线程局部变量副本的隐式引用;线程终止后,它的所有线程本地实例副本都将会被垃圾回收(除非存在对这些副本的其他引用),类图如下。

image.png

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,均是 ThreadLocal.ThreadLocalMap 类型的变量。ThreadLocalMap 有自己独立的实现,可以简单的将它的 key 是作为 ThreadLocal,value 为代码中放入的值。默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一调用 ThreadLocalMap 的 set 或 get 方法时才会创建它们。其实每个线程的本地变量不是存放在 ThreadLocal 实例里面的,而是存放在调用线程的 threadLocals 变量里的面。也就是说,ThreadLocal 类型的本地变量存放在具体的线程内存中,从而实现了现成的隔离。

ThreadLocal 就是一个工具,它通过 set 方法把 value 值放入调用线程的 threadLocals 中存储起来。如果调用线程一直不终止,那么这个变量会一直存放在调用线程的 threadLocals 中,所以当不需要使用本地变量时,需要调用 ThreadLocal 的 remove 方法,从当前线程的 threadLocals 中删除该本地变量。

2、构造方法

ThreadLocal 只提供了一个无参构造,创建 ThreadLocal 变量。

  1. public ThreadLocal() {
  2. }

3、属性

其中 threadLocalHashCode 是 final 修饰的变量,在 ThreadLocal 被创建的时候就会调用 nextHashCode 方法初始化,每调用一次 threadLocalHashCode 就会增加 HASH_INCREMENT = 0x61c88647,0x61c88647 的十进制为 1640531527。

  1. // 用于计算 ThreadLocalMap 的桶的 hash 值
  2. private final int threadLocalHashCode = nextHashCode();
  3. // 从 0 开始
  4. private static AtomicInteger nextHashCode = new AtomicInteger();
  5. // 0x61c88647 = 1640531527,近似等于 ~(2^32 * 0.618) + 0b1
  6. // 每次当前线程的 nextHashCode 增加的魔法值
  7. private static final int HASH_INCREMENT = 0x61c88647;

下面的代码过程演示了,0x61c88647 和斐波那契散的关系,当我们用 0x61c88647 作为魔数累加为每个 ThreadLocalMap 分配各自的 threadLocalHashCode,再与 2 的幂次方,得到的结果分布很均匀。ThreadLocalMap 使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用 slot,从而保证效率。这就是为什么 table 数组的长度必须是 2 的幂次方原因,为了优化探测效率。

  1. // 黄金分割数* 2^32 = 0.618 * 2^32
  2. long lg = (long) ((1L << 32) * ((Math.sqrt(5) - 1) / 2));
  3. // 32位无符号整数
  4. System.out.println("as 32 bit unsigned: " + lg);
  5. int it = (int) lg;
  6. // 32位有符号整数
  7. System.out.println("as 32 bit signed: " + it);
  8. System.out.println("MAGIC = " + 0x61c88647);
  9. =====================执行结果=====================
  10. as 32 bit unsigned: 2654435769
  11. as 32 bit signed: -1640531527
  12. MAGIC = 1640531527

4、方法

4.1、nextHashCode

该方法主要用于:当前线程的 nextHashCode 与魔法值 HASH_INCREMENT 相加。每调用一次该方法,当前线程的 nextHashCode 就会加一次 HASH_INCREMENT。

  1. private static int nextHashCode() {
  2. return nextHashCode.getAndAdd(HASH_INCREMENT);
  3. }

4.2、initialValue

该方法为 ThreadLocal 要保存的数据类型指定一个初始值,在 ThreadLocal 中默认返回 null,我们可以通过重新该方法进行数据的初始化。该方法会在 get 和 set 方法调用。JDK8 提供的 Supplier 函数接口会更加简化。

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

4.3、withInitial

JDK8 中提供的 Supplier 函数接口的方法

  1. public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
  2. return new SuppliedThreadLocal<>(supplier);
  3. }

4.4、get

该方法用于返回当前线程在 ThreadLocal 中的数据备份,当前线程的数据都存放在一个成为 ThreadLocalMap 的数据结构中。

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. // 从 ThreadLocal 中获取当前线程的 ThreadLocalMap
  4. ThreadLocalMap map = getMap(t);
  5. // 从 map 中获取当前线程运行过程中的备份
  6. if (map != null) {
  7. ThreadLocalMap.Entry e = map.getEntry(this);
  8. // 判断 entry 是否无效
  9. if (e != null) {
  10. @SuppressWarnings("unchecked")
  11. T result = (T)e.value;
  12. // 如果有效,则返回 e.value
  13. return result;
  14. }
  15. }
  16. // 返回之后,重新将 ThreadLocalMap 设置为 initValue 方法返回的值
  17. return setInitialValue();
  18. }

4.5、setInitialValue

设置当前线程 ThreadLocal 的 ThreadLocalMap 的初始值,数据的初始值是 initialValue 方法返回的结果。

  1. private T setInitialValue() {
  2. // 获取初始化值
  3. T value = initialValue();
  4. Thread t = Thread.currentThread();
  5. // 获取当前线程的 ThreadLocalMap
  6. // 如果是父线程则返回 Thread.threadLocals
  7. // 如果非父线程则返回 Thread.inheritableThreadLocals
  8. ThreadLocalMap map = getMap(t);
  9. // 如果不为空,直接设置初始值
  10. if (map != null)
  11. map.set(this, value);
  12. // map 为空,则创建一个新的 ThreadLocalMap
  13. else
  14. createMap(t, value);
  15. return value;
  16. }

4.6、set

该方法主要是为了 ThreadLocal 指定将要被存储的数据,如果重写了 initialValue 方法,在未调用 set 方法的时候,数据的初始值是 initialValue 方法返回的结果。

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }

4.7、remove

移除此线程 ThreadLocalMap 中当前线程的值

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. m.remove(this);
  5. }

4.8、getMap

获取与 ThreadLocal 关联的映射,该方法在 InheritableThreadLocal 中重写。

  1. ThreadLocalMap getMap(Thread t) {
  2. // 直接返回线程 t 的 threadLocals
  3. return t.threadLocals;
  4. }

4.9、createMap

创建与 ThreadLocal 关联的映射,该方法在 InheritableThreadLocal 中重写。

  1. void createMap(Thread t, T firstValue) {
  2. // 调用构造方法构建线程 t 的 ThreadLocalMap
  3. t.threadLocals = new ThreadLocalMap(this, firstValue);
  4. }

4.10、createInheritedMap

该方法主要就是以父线程的 inheritableThreadLocals 为数据源,过滤出有效的 entry,初始化到自己的 threadLocals 中。其中 childValue 可以被重写。

需要注意的地方是 InheritableThreadLocal 只是在子线程创建的时候会去拷一份父线程的 inheritableThreadLocals。如果父线程是在子线程创建后再 set 某个 InheritableThreadLocal 对象的值,对子线程是不可见的。

  1. static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
  2. return new ThreadLocalMap(parentMap);
  3. }

4.11、childValue

该方法是在子类 InheritableThreadLocal 中实现的,但是在这里内部定义是为了提供给 createInheritedMap 工厂方法使用的。

  1. T childValue(T parentValue) {
  2. throw new UnsupportedOperationException();
  3. }

5、InheritableThreadLocal

同一个 ThreadLocal 变量在父线程 pT 中被设置值后,在子线程中是获取不到的。因为子线程 subT 里面调用 get 方法时,当前线程是 subT 线程;而父线程 pT 调用 set 方法设置线程变量的是 pT 线程,两者不是同一个线程。所以子线程无法获取到父线程的 threadLocals 中存放的本地变量。可以通过 InheritableThreadLocal 类来解决这个问题。

5.1、类图结构

InheritableThreadLocal 继承自 ThreadLocal,类图如下。其提供了一个特性,就是让子线程可以访问父线程中设置的本地变量。

image.png

5.2、源码

从下面的源码可以看出,InheritableThreadLocal 继承了 ThreadLocal,并重写了三个方法。这个三个方法实现了在 InheritableThreadLocal 中,线程的变量 inheritableThreadLocals 代替了 threadLocals。

createMap 方法,当第一调用 set 方法时,创建的是当前线程的 inheritableThreadLocals 变量的实例而不再是 threadLocals。getMap 方法,当调用 get 方法获取当前线程内部的 map 变量时,获取的是 inheritableThreadLocals 而不再是 threadLocals。

  1. public class InheritableThreadLocal<T> extends ThreadLocal<T> {
  2. // 计算此可继承线程局部变量的子级初始值,作为创建子线程时父级值的函数。
  3. // 在启动子线程之前,从父线程内部调用此方法。
  4. // 此方法仅返回其输入参数,如果需要不同的行为,则应覆盖该方法。
  5. protected T childValue(T parentValue) {
  6. return parentValue;
  7. }
  8. // 获取与 ThreadLocal 关联的映射
  9. ThreadLocalMap getMap(Thread t) {
  10. return t.inheritableThreadLocals;
  11. }
  12. // 创建与 ThreadLocal 关联的映射
  13. void createMap(Thread t, T firstValue) {
  14. t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
  15. }
  16. }

�childValue 方法何时执行,以及如何让子线程可以访问父线程的本地变量?这要从创建 Thread 开始看起,下面是创建 Thread 类的源码。

在创建线程时,构造函数会调用 init 方法,对线程进行初始化。代码 23 行会先判断父线程的 inheritableThreadLocals 是否为 null?如果不为 null 的话,这里会直接调用 ThreadLocal 的 createInheritedMap 方法创建一个新的 ThreadLocalMap 实例,然后赋值给子线程的 inheritableThreadLocals 属性。childValue 最终会在 ThreadLocalMap 的构造函数中被调用,在改构造函数中会把父线程的 inheritableThreadLocals 属性的值赋值到新的 ThreadLocalMap 实例中。

  1. public Thread() {
  2. init(null, null, "Thread-" + nextThreadNum(), 0);
  3. }
  4. private void init(ThreadGroup g, Runnable target, String name,
  5. long stackSize) {
  6. init(g, target, name, stackSize, null, true);
  7. }
  8. private void init(ThreadGroup g, Runnable target, String name,
  9. long stackSize, AccessControlContext acc,
  10. boolean inheritThreadLocals) {
  11. if (name == null) {
  12. throw new NullPointerException("name cannot be null");
  13. }
  14. this.name = name;
  15. // 获取当前线程
  16. Thread parent = currentThread();
  17. ...
  18. // 如果是父线程的 inheritableThreadLocals 不为 null
  19. // 设置子线程的 inheritableThreadLocals
  20. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  21. this.inheritableThreadLocals =
  22. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  23. ...
  24. }

6、内存泄漏分析

ThreadLocal 会引起内存泄漏原因是,如果一个线程的 ThreadLocal 对象被回收了,我们之前保存的 value 对于当前线程是可达的,如图所示,Current Thread Ref -> Thread -> ThreadLocalMap -> Entry.value 的一条强引用链时可达的,因此 value 不会被回收。

image.png

内存泄漏和内存溢出是有区别的,内存泄漏是导致内存溢出的原因之一。但两者并不是完全等价的,内存泄漏更多的是程序不再持有某个对象的引用,但是该对象仍然无法别垃圾回收器回收。原因是该对象的引用根 root 的链路是可达的,比如 Current Thread Ref 到 Entry.value 的引用链路

认为 ThreadLocal 不会引起内存泄漏的原因是 Thread.ThreadLocalMap 源码实现中自带一套自我清理的机制。之所以有关于内存泄漏的讨论,是因为在有线程复用的场景中,一个线程的寿命很长,此时大对象长期不被回收可能会影响系统运行效率和安全,如线程池。如果线程不会复用,用完立即销毁了,不会有 ThreadLocal 引发内存泄漏的问题。

7、总结

ThreadLocal 不是用来解决线程安全问题的,多线程不同享,不存在竞争问题。目的是线程本地变量只能单个线程维护使用。在使用 ThreadLocal 的过程中,防止内存泄漏最好的方法就是结束时调用其 remove 清理 ThreadLocal 中的无效 Entry。比如:对于一个请求一个线程的 tomcat 中,在代码中对 web api 做一个切面,存放一些用户信息,在连接点方法结束后,再显式调用 remove 进行无效 Entry 清理。

InheritableThreadLocal 类通过重写 ThreadLocal 的 creatMap 和 getMap 方法,实现了让本地变量保存到具体线程的 inheritableThreadLocals 属性中。此时线程通过 InheritableThreadLocal 实例的 set 或 get 方法设置变量时,就会设置当前线程的 inheritableThreadLocals 属性。当父线程创建子线时,构造函数会把父线程中 inheritableThreadLocals 属性中的本地变量值复制一份保存到子线程的 inheritableThreadLocals 属性中。