线程本地存储 ThreadLocal 的原理及使用
背景
多个线程并发读写同一共享变量会存在一些问题,只要我们突破共享变量就不会有并发问题。除了使用局部变量外,Java语言提供的线程本地存储(ThreadLocal)就能解决多线程共享变量问题。
使用
下面我们以并发场景下使用线程不安全的 SimpleDateFormat 为例。
static class SafeDateFormat {
// 定义ThreadLocal变量
static final ThreadLocal tl = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
static DateFormat get() {
return tl.get();
}
}
// 不同线程执行下面代码返回不同的DateFormat
DateFormat df = SafeDateFormat.get();
不同线程调用 SafeDateFormat.get() 将返回不同的 SimpleDateFormat 对象实例,由于不同线程的 SimpleDateFormat 相互隔离,所以是线程安全的。
原理
如下图所示,Thread 持有一个 ThreadLocalMap 的引用,ThreadLocalMap 以 ThreadLocal 为 key,任意对象为 value。
部分源码如下:
class Thread {
// 内部持有 ThreadLocalMap 的引用
ThreadLocal.ThreadLocalMap threadLocals;
}
class ThreadLocal {
public T get() {
// 首先获取线程持有的 ThreadLocalMap
ThreadLocalMap map = Thread.currentThread().threadLocals;
// 在 ThreadLocalMap 中查找变量
Entry e = map.getEntry(this);
return e.value;
}
static class ThreadLocalMap {
// 内部是Entry数组而不是Map
Entry[] table;
// 根据 ThreadLocal 查找 Entry
Entry getEntry(ThreadLocal<?> key) {
...
}
// Entry定义
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
}
}
}
几点说明(理解原理的关键)
- 为什么 ThreadLocalMap 放在 Thread,而不放在 ThreadLocal 中呢?
试想一下,如果 ThreadLocalMap 放在 ThreadLocal 中(此时 Thread 成为 ThreadLocalMap 的 key),Thread 结束了生命周期,而此时 ThreadLocal 还没有结束生命周期,由于 ThreadLocal 引用了 ThreadLocalMap,ThreadLocalMap 引用了 Thread,使得 Thread 无法被垃圾收集器回收,导致内存泄漏。 - 为什么 Entry 对 ThreadLocal 是弱引用(WeakReference)?
试想一下,如果 ThreadLocal 结束了生命周期,而此时 Thread 还没有结束生命周期(从线程池中获取线程),由于 Thread -> ThreadLocalMap -> entry -> key(ThreadLocal) 的引用存在,使得 ThreadLocal 无法被回收,有了弱引用之后,ThreadLocal 只能存活到下次GC。 - value 为什么不能被回收?
Entry 中的 value 是被 Entry 强引用的,所以即便 value 的生命周期结束了,value 也是无法被回收的,从而导致内存泄露。 - 那要如何回收 value 呢?
手动remove。
try {
...
}
finally {
// 手动清理 ThreadLocal
tl.remove();
}
拓展
通过 ThreadLocal 创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过 ThreadLocal 创建了线程变量 V,而后该线程创建了子线程,你在子线程中是无法通过 ThreadLocal 来访问父线程中的线程变量 V 。
如果你需要子线程继承父线程的线程变量,那该怎么办呢?Java 提供了 InheritableThreadLocal,InheritableThreadLocal 是 ThreadLocal 子类,所以用法和 ThreadLocal 相同,但由于存在和 ThreadLocal 同样的内存泄漏问题,不建议使用。
如果需要使用,可以考虑使用阿里的 TransmittableThreadLocal 框架。