并发 - ThreadLocal

ThreadLocal

作用

有一个比喻:

学生需要在签字墙签字, 相当于只有一个签字笔,学生们需要争抢这个签字笔;而 ThreadLocal 相当于给每个学生发了一个签字笔,每人一个,效率大大提升。

底层原理

假如你自己需要实现 ThreadLocal<T> 相关的 API,请问你会怎么做?

  • 你可能会使用 ConcurrentHashMap<Thread, T>,以 Thread.currentThread() 作为 key,这完全可以。但是缺点明显:1. 处理并发问题;2. 必须有指针指向 Thraed 和 这个对象,即使 Thraed 已经结束了,可以被 GC 了。
  • 那我们改为 Collections.synchronizedMap(new WeakHashMap<Thread, T>()) 怎样? 可以解决 GC 问题,但是多线程问题仍然没有解决。

Java 实现的想法,没有用 <Thread, T> ,而是大概如下:

new WeakHashMap<ThreadLocal,T>()

而事实上,在每个 Thread 内部也的确有这么一个 Map 指针:

public class Thread implements Runnable {
    
    ThreadLocal.ThreadLocalMap threadLocals = null;

}

虽然 ThreadLocalMap 并不是一个 WeakHashMap,但是它的设计类似 WeakHashMap,它的 Key 是由 Weak Reference 引用的。

再看 ThreadLocal.get() 方法:

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.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;
}

假设你定义了如下几个 ThreadLocal

ThreadLocal<SimpleDateFormat> threadLocalA = new ThreadLocal<>();
ThreadLocal<Random> threadLocalB = new ThreadLocal<>();
ThreadLocal<Buffer> threadLocalC = new ThreadLocal<>();

那么 Thread.currentThread() 指向了一个 ThreadLocalMap,这个 Map 存储了 3 个 ThreadLocal

使用场景

  • 你想要在多线程环境下访问某个非线程安全的对象(例如 SimpleDateFormat),但是你又想避免通过添加 synchronized 来进行同步,此刻可以考虑使用 ThreadLocal 来给每个线程都搞一份找个对象的实例。
  • 很多框架使用 ThreadLocal 来存储与当前线程相关的上下文信息 Context,这样从 A 方法调用到 B 方法的时候,不用手动通过参数传入当前的 Context 信息,直接从 ThreadLocal 中读取即可。

内存泄露

ThreadLocal 已经被赋值为 null 了,但是由于 Thread 没有运行完,一直强引用着 ThreadLocalMap,那么这个 Map 无法被 GC,导致这个 Map 所引用的 value 无法被回收(key 是可以被回收的,因为 key 是弱引用),从而出现内存泄露。

ThreadLocalMap 中的每一项如下所示,即 keyWeakReference,而 v 也就是 value 是强引用:

static class Entry extends WeakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */
    Object value;

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

}

为什么 value 不也弄成弱引用? 如果弄成弱引用,那么每次 GC 都有可能被清空,那么就无法自始至终地保存这一个 value 对象了,每次获取到的有可能都是一个全新的对象。

推荐:每次使用完 ThreadLocal,都调用 ThreadLocal.remove() 方法清除数据。

objectThreadLocal.set(userInfo);
try {
    // ...
} finally {
    objectThreadLocal.remove();
}