使用方法

ThreadLocal 的使用方法非常简单,声明 ThreadLocal 对象,一般是全局变量,然后调用其 get()、set()、remove() 即可。

如果你想给 ThreadLocal 的 get() 方法增加默认值逻辑,可以通过下面两个方式:

1、继承 LocalThread 类重写 initialValue() 方法:

public class Local extends ThreadLocal<Object> {

    @Override
    protected Object initialValue() {
        return new Date();
    }
    
}

2、调用 ThreadLocal 的静态方法 withInitial,它帮我们重写了 initialValue() 方法:

public class Test {
    
    ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(new Supplier<Object>() {
        @Override
        public Object get() {
            return null;
        }
    });
    
}

源码分析

ThreadLocal 的源码非常简单,只需要从 get()、set()、remove() 三个方法开始即可。

set方法

public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程对象
    ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
    if (map != null)
        map.set(this, value); // k 为当前 ThreadLocal 对象
    else
        createMap(t, value); // 初始化 ThreadLocalMap 并赋值
}

看完这段代码的感受就是 ThreadLocal 有点工具类的味道,真正存储 value 的 ThreadLocalMap 是 Thread 的内部属性。

ThreadLocal 其实并没有对 value 有任何引用。

让我们进入 ThreadLocalMap 的 set() 方法内部,它里面的代码才是比较有意思的地方。

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get(); // 获取 Entry 的 k 的内存索引
        
        if (k == key) { // 如果 Entry 的 k 与 key 相同,即同一个 ThreadLocal 对象在调用
            e.value = value;
            return;
        }

        if (k == null) { // 有可能存在 k 等于 null 的情况,比如之前的 ThreadLocal 对象被回收了
            replaceStaleEntry(key, value, i); // 这一步会帮忙释放 ThreadLocal 被 GC 回收后未释放的 value 们
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

get方法

public T get() {
    Thread t = Thread.currentThread(); // 获取当前线程对象
    ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 当前 ThreadLocal 对象作为 k,取到 v
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue(); // 执行 initialValue 方法并构建 ThreadLocalMap 放入当前线程
}

如果没有调用过 set() 方法,那么第一次调用 get() 方法会执行 setInitialValue() 方法初始化 ThreadLocalMap 对象。

让我们再次进入 ThreadLocalMap 的 getEntry() 方法:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) // Entry 的 k 与 key 相同,即同一个 ThreadLocal 对象在调用
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

如果通过 key 没找到 v,那么 getEntryAfterMiss() 会怎么做呢?

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

    while (e != null) { // 如果 e 为 null,证明确实没有 set 过,返回 null 即可
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null) // set 过,但是之前的 ThreadLocal 对象被 GC 回收了
            expungeStaleEntry(i); // 这一步会帮忙释放 ThreadLocal 被 GC 回收后未释放的 value 们
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread()); // 获取当前线程的 ThreadLocalMap 属性
    if (m != null)
        m.remove(this); // 当前 ThreadLocal 对象作为 k
}

remove() 还是比较简单的,再看下 ThreadLocalMap 的 remove() 方法:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i); // 虽然前面有 if 语句,但这个方法依旧会帮忙释放 ThreadLocal 被 GC 回收后未释放的 value 们
            return;
        }
    }
}

总结梳理

看完源码,相信你也晕了,不过不要紧,有图帮你梳理:

回到本文的主题,咱们边看图边总结一下 ThreadLocal 内存回收相关的结论:

  1. 当 ThreadLocal == null 时,ThreadLocal 由于被 WeakReference 包裹,所以 ThreadLocal 会被 GC 回收。

  2. 当 Value == null && ThreadLocal !=null 时,value 由于还被当前线程引用着,所以只要当前线程没有 exit() 退出 或者 ThreadLocal 没有调用 remove() 方法移除 Value, Value 就不会被 GC 回收。这就是造成内存泄露的原因。

  3. 如果 Thread 执行完 Runnable 后就退出,并且 Value == null 成立,Value 会被 GC 回收。 但由于现在都在使用线程池管理线程,线程执行完 Runnable 后并不会退出,并且 ThreadLocal 也大都是 final 修饰的全局变量,所以在使用 ThreadLocal 时,记得调用 remove() 方法防止 Value 内存泄露。

最后一个问题,ThreadLocalMap.Entry 的 Key(即 ThreadLocal)使用弱引用的原因:

如果没有调用 ThreadLocal 的 remove() 方法,那么当 Value == null && ThreadLocal == null 成立时,当前线程也没有退出的话,会存在内存泄露。但当其它 ThreadLocal 对象在同一个线程调用 get()/set()/remove() 方法时,会检测出 ThreadLocal 被回收的 Entry(即 e.get() == null),并将 e.value 释放。(PS:如果 ThreadLocal 在你的项目里是 final 修饰的全局变量,那这一步就挺鸡肋的。)