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 节点引用
















