跳至主要內容

JUC 八股6 - ThreadLocal2

codejavajuc八股约 1019 字大约 3 分钟

ThreadLocal2

内存泄露

ThreadLocalMap 的 Key 是 弱引用,但 Value 是强引用。

如果一个线程一直在运行,并且 value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。

问题场景:ThreadLocal 对象本身没人引用了,但线程还在运行(比如线程池的核心线程)

解决内存泄露

使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间

try {
    threadLocal.set(value);
    // 执行业务操作
} finally {
    threadLocal.remove(); // 确保能够执行清理
}

remove() 会调用 ThreadLocalMap 的 remove 方法遍历哈希表,找到 key 等于当前 ThreadLocal 的 Entry,找到后会调用 Entry 的 clear 方法,将 Entry 的 value 设置为 null

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算 key 的 hash 值
    int i = key.threadLocalHashCode & (len-1);
    // 遍历数组,找到 key 为 null 的 Entry
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 将该 Entry 的 key 置为 null(即 Entry 失效)
            e.clear();
            // 清理过期的 entry
            expungeStaleEntry(i);
            return;
        }
    }
}

public void clear() {
    this.referent = null;
}

然后执行 expungeStaleEntry() 方法,清除 key 为 null 的 Entry

什么时候 remove

不是每次操作都 remove,主要是根据使用场景来决定的。在一些短生命周期的场景中,比如处理单个 HTTP 请求的上下文信息,我通常会在请求结束时统一 remove

但在一些需要跨多个方法调用保持状态的场景中,就不会每次都 remove。

我的使用原则是:

  • 在方法级别使用时,try-finally 保证 remove
  • 在请求级别使用时,通过拦截器或 Filter 统一清理
  • 如果存储的对象比较大,使用完立即 remove
  • 定期检查 ThreadLocal 的使用情况,避免遗漏

key 为什么要弱引用

弱引用的好处是,当内存不足的时候,JVM 能够及时回收掉弱引用的对象

key 是弱引用,new WeakReference(new ThreadLocal()) 是弱引用对象,当 JVM 进行垃圾回收时,只要发现了弱引用对象,就会将其回收。

一旦 key 被回收,ThreadLocalMap 在进行 set、get 的时候就会对 key 为 null 的 Entry 进行清理

总结一下,在 ThreadLocal 被垃圾收集后,下一次访问 ThreadLocalMap 时,Java 会自动清理那些键为 null 的 entry,这个过程会在执行 get()、set()、remove()时触发

分析

问题场景:ThreadLocal 对象本身没人引用了,但线程还在运行(比如线程池的核心线程)

value 强引用是避免数据丢失,如果我们还要用到

如果 value 也是弱引用

ThreadLocal<User> tl = new ThreadLocal<>();
tl.set(new User("111"));

// 假设没有其他强引用指向这个 User
User u = null;

下次 GC 时:

  • User 对象被回收(只有 Entry.value 指着它,是弱引用)
  • 你再 tl.get() 拿到 null

如果 value 是弱引用,用户根本无法控制何时被回收,这个 API 就没法用了

所以,虽然 map 里的 key 是弱引用,可能会被 gc 回收 (仅当 ThreadLocal 对象本身没人引用,只有这个弱引用在)

那么说明这个本身就没有意义了,所以 get() 时会检测这些没有意义的数据,清理

// ThreadLocal.java
public T get() {
    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;
        }
    }
    return setInitialValue();
}

static class ThreadLocalMap {
    ...
    private Entry[] table;
    ...
    private Entry getEntry(ThreadLocal<?> key) {
        // 根据这个 threadlocal 的hash算对应索引为止
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // 判断 Entry 的弱引用 key 是否还指向 key 这个对象
        if (e != null && e.refersTo(key))
            return e;
        else
            // key 可能被gc了获取 e==null
            return getEntryAfterMiss(key, i, e);
    }
    ...
}

当通过 hash & (len-1) 计算出的位置上没找到目标 ThreadLocal,或者该位置被其他 ThreadLocal 占了,就需要线性探测往后找

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        if (e.refersTo(key))
            return e;
        if (e.refersTo(null))
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
上次编辑于: