关于 ThreadLocal

用法

ThreadLocal 包装了一个 get 和 set 方法,在当前线程内,可以 get 到之前 set 进的值:

ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("a");
String a = tl.get();

实现

ThreadLocal 的本质是每个线程有一个专属的 map,存取 ThreadLocal 对象时,会以该 ThreadLocal 对象为 key,存入的值为 value 进行存取。

get 和 set 方法

get 和 set 的代码如下:

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

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private void set(ThreadLocal<?> key, Object value) {
    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.
    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();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

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();
}

可以看出,即使同一个 ThreadLocal 对象在不同线程中进行 set,它的 get 方法取出的值也互不打扰。因为 ThreadLocal 的 set 方法并不是把一个值跟当前 ThreadLocal 对象关联起来,而是将当前 ThreadLocal 对象作为 key,在当前线程专属的 map 中和值进行关联。因此不同线程中进行 set 时,都会存入不同的 map 中。

Map 的实现

ThreadLocal 使用的 Map 不是 HashMap,而是自己实现的 Map。它将 key 和 value 封装成一个 Entry,然后用一个 table 来存该 Entry。获取索引的方式和 HashMap 类似,它是用 threadLocalHashCode 和 table 长度-1进行与操作,因此 table 的长度也需要是2的幂。当冲突时,它没有用链表,而是直接计算下一个地址:((i + 1 < len) ? i + 1 : 0)。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

WeakReference

包裹 ThreadLocal 和 Value 的 Entry 类继承自 WeakReference。WeakReference 的特点是,如果对象除了有弱引用指向它后没有其他强引用关联它,当进行年轻代垃圾回收时,该引用指向的对象就会被垃圾回收器回收。

先说为什么要用弱引用:

  • 如果使用正常的引用方式的话,线程在使用过一个 ThreadLocal 对象并释放后,它所在的 Entry 对象,依然被线程的 ThreadLocalMap 所持有,同时 ThreadLocal 对象被 Entry 所持有,无法被回收。由于线程会被复用,ThreadLocalMap 中的 Entry 会越来越多,而且里面有大量的 Entry 是不会被访问的,因此造成了所谓的内存泄漏。
  • 如果使用弱引用的话,线程在使用过一个 ThreadLocal 对象并释放后,在经历过一次 Yong GC 后,就会被回收。这样,只需要在 set 和 get 方法里对 Entry 的 ThreadLocal 对象做 null 值的判断,就能去清除这些 key 为 null 的 Entry 对象。这里注意,WeakReference 对象在经历 GC 时被回收的是它的 referent 对象,它本身被 Map 所持有,不会被回收。

内存泄漏

现在网上很多说法是,此时留下 value 就是内存泄漏,因为没法访问这个 value,它还占着内存。因此每次使用完后需要 tl.remove() 来移除这个 Entry,阿里的规范也是这么要求的。这样做是不错,不过我觉得这么做的话弱引用就没意义了,因为如果 map 是强引用的话,每次使用完后 remove,也能起到效果。同时,每次 get 和 set 时,本身也会移除 key 为 null 的 Entry。

不过根据这篇文章,get 和 set 并不能完全触达所有的 key 为 null 的 Entry,所以建议还是要 remove 掉不再使用的 ThreadLocal 变量。

线程资源泄漏

ThreadLocal 还会有个潜在的问题,就是如果线程进行了复用,比如线程池这种,线程在执行下一个方法时会带着持有上一个线程数据的 Map。可以通过下面的例子验证:

public class ThreadLocalTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<String> tl = new ThreadLocal<>();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(tl.get());
                tl.set(LocalDateTime.now().toString());
                System.out.println(tl.get());
            }
        };
        ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(1);
        threadPoolExecutor.submit(runnable);
        Thread.sleep(1000);
        threadPoolExecutor.submit(runnable);
    }
}

输出的结果是:

null
2022-03-15T14:48:02.310
2022-03-15T14:48:02.310
2022-03-15T14:48:03.233

不过这个本来就不是 bug 而是个 feature,如果真的是不能让其他线程拿到的数据,只要注意在 ThreadLocal 的生命周期在方法栈内即可。

用途

Spring 的 @Transaction 本质就是用 ThreadLocal 实现的。

posted @ 2022-03-16 17:34  青石向晚  阅读(33)  评论(0编辑  收藏  举报