关于 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 实现的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义