ThreadLocal 随笔

写在前面

ThreadLocal 通常是将类的私有静态变量(全局唯一并且基本不会发生改变)与之绑定,方便上下文信息交互。比如 TransactionId 或 userId 等。

一个线程可以声明多个 ThreadLocal 对象,使用 ThreadLocalMap 进行维护。

ThreadLocalMap<ThreadLocal, V> 是一个自定义的 hashMap。 ThreadLocal 对象做 key,方便查询时快速定位到某个ThreadLocal节点。

工作原理

扩容机制

ThreadLocalMap 在内部节点的数量大于阈值(总数量 * 2/3)时,会尝试进行 rehash。首先清除当前无效的数据(索引下标对象不为空,并且调用 get 方法无法获取到对象),如果清除之后的数据仍大于总容量的 1/2,就会执行 resize 方法,容量为原来的二倍,同时重新计算对象在新坐标的位置(hashCode & len)

内部同样采用二的整数幂作为计算单位,新 table 的位置也总是以 2 的整数幂作为单位偏移

如何实现与线程绑定?

Thread 内部有 threadLocals 和 inheritableThreadLocals 的 ThreadLocalMap 全局变量,在线程调用方调用 set 方法时,首先获取当前线程下的 threadLocals ,尝试插入到该 map 中,如果不存在,则创建新的

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
  map.set(this, value);
else
  createMap(t, value);

// getMap 私有方法
ThreadLocalMap getMap(Thread t) {
   return t.threadLocals;
}

// createMap 私有方法
void createMap(Thread t, T firstValue) {
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果 get 方法被调用时,threadLocalMap 还未初始化,如何避免返回 NULL ?

ThreadLocal 提供了一个静态方法:withInitial(Supplier supplier) 。内部实现为 SupplierThreadLocal,重写了 ThreadLocal 的 initialValue() 方法,从而实现在获取结果时,如果线程的 threadLocals 未初始化,在初始化 map 的同时,插入当前 threadLocal 的初始化值

Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
				// 静态方法重写该方法,在调用时,就会 call 声明方声明的 supplier 函数,拿到结果进行插入 map
        return setInitialValue();

// setInitialValue 方法
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
// supplierThreadLocal 
 static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

如何用 ThreadLocal 实现父子线程数据共享?

Thread 内部维护了两个全局变量:threadLocals 和 inheritableThreadLocals。分别代表了当前线程的本地缓存变量和子线程的缓存变量

ThreadLocal 提供了一个新的实现类:InheritableThreadLocal。当线程内使用该对象进行初始化 map 时,会给 inheritableTheadLocals 赋值。并且返回 map 的引用也是指向了该对象。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
  
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

Thread 的初始化方法有这么一段代码:

如果父线程内的 inheritabeThreadLocals 不为空,那么子线程也会给 inheritableThreadLocals 赋值,并且将父线程当前 map 的数据快照作为初始化对象,插入到当前线程内。以此向下给子线程传递。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  this.inheritableThreadLocals =
  ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

如此就实现了父子线程数据共享

总结:

如果业务场景需要父子线程数据传递,就可以使用 inheritableThreadLocal 对象。但是,子线程只会记录在声明时,父线程的本地数据对象快照,如果在声明之后,父线程插入了新数据,则不会在子线程中展现

实际可能会遇到的问题

1、ThreadLocal 的内存泄露问题

从 ThreadLocal 源码分析出现该问题的原因:

第一点:ThreadLocalMap 的 rehash 惰性回收机制问题

ThreadLocalMap 是自定义实现的 hash map。扩容的条件为达到 2/3 容量时,才会进行扩容。如果 Entry 的 key 为 null,此时 map 不会主动将 key 为 null 节点的 value 值置为 null,只有当 rehash 遍历 table 时,才会清除无效的 Entry 节点

第二点:ThreadLocalMap 的 Entry节点 key 为 WeakReference 引用的 ThreadLocal 对象。

根据弱引用规则,有 GC 则回收。当 ThreadLocal 如果不是强引用,当出现 GC 时,会立马被回收。此时 value 就会无法被访问到,但又无法被 GC,因为存在 Entry 节点的强引用,因为 Thread 内部的 threadLocals 为全局变量(在 rehash 之前)。这也可以称之为内存泄露的场景之一

static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
  Object value;

  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}

总结:

根据以上源码分析,为了避免 threadLocal 对象的重复创建,并且在使用过程中也是采用静态方法,所以在声明时会将 ThreadLocal 声明为 static 对象。

在 Entry 强引用的情况下,key 为 NULL,如果不触发扩容,Entry 的 value 对象将永远不可达,并且不会被 GC,所以称之为 ThreadLocal 的内存泄露

解决方案:

在线程结束或方法结束时手动调用 remove 方法,手动清除 map 中当前的 Entry 节点引用