ThreadLocal源码解析
ThreadLocal也是面试题中十分常见的问题,我是在大部分人的启蒙项目苍穹外卖接触到ThreadLocal这个概念的,苍穹外卖把用户id存在ThreadLocal中来进行权限检验,这是我所了解到的ThreadLocal的第一个用处。
在上次字节面试时被面试官狠狠拷打了ThreadLocal,从概念问到了底层,而我只背了八股,了解到ThreadLocal底层是个map,有弱引用,在面试官反复的反问上败下阵来,今天又是我一个小实习生摸鱼的一天,决定看一看ThreadLocal源码,看一看他到底是何方神圣。
ThreadLocal简介
ThreadLocal 是 Java 中用于实现线程局部变量的类,它能够在多线程环境下,为每个线程提供独立的变量副本,从而避免线程间的数据竞争。
核心作用
-
线程隔离:每个线程操作自己的变量副本,互不干扰。
-
避免同步:不需要加锁(如 synchronized
synchronized
),提高并发性能。 -
典型应用:
-
Spring 的事务管理(TransactionSynchronizationManager)
-
用户会话信息存储(如 Session)
-
JDBC 连接管理(避免线程间共享Connection)
-
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
ThreadLocal原理
ThreadLocal的set()方法
public void set(T value) {Thread t = Thread.currentThread(); // 获取当前线程ThreadLocalMap map = getMap(t); // 获取线程的 ThreadLocalMapif (map != null) {map.set(this, value); // 如果 map 已存在,直接设置值} else {createMap(t, value); // 否则初始化 ThreadLocalMap}
}
从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
那么第二个问题来了,ThreadLocalMap是什么?
ThreadLocalMap实现
static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object). Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table. Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}}
可看出ThreadLocalMap是ThreadLocal的内部静态类,用于存储线程的局部变量,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用(常用考点)。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。每个线程都有一个独立的 ThreadLocalMap,存储该线程的所有 ThreadLocal 变量。
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)]) {if (e.refersTo(key)) { // 如果 key 已存在,更新 valuee.value = value;return;}if (e.refersTo(null)) { // 如果 key 已被回收(弱引用),替换过期 EntryreplaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value); // 插入新 Entryint sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold) {rehash(); // 扩容}
}
ThreadLocalMap的getEntry()方法
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key) {return e; // 直接命中} else {return getEntryAfterMiss(key, i, e); // 线性探测查找}
}
ThreadLocal的get()方法
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this); // 获取当前 ThreadLocal 对应的 Entryif (e != null) {@SuppressWarnings("unchecked")T result = (T) e.value;return result;}}return setInitialValue(); // 如果不存在,初始化并返回默认值
}private T setInitialValue() {T value = initialValue(); // 默认返回 null,可重写Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}return value;
}
ThreadLocal的remove()方法
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this); // 从 ThreadLocalMap 中移除当前 ThreadLocal}
}
调用 remove() 方法会直接从当前线程的 ThreadLocalMap
中删除对应的 ThreadLocal
键值对。这一操作的核心目的是防止内存泄漏。
ThreadLocal 的内存泄漏问题
ThreadLocalMap使用 ThreadLocal的弱引用作为 key,弱引用的特性是:如果某个对象仅被弱引用关联,垃圾回收(GC)时会自动清理该对象。因此,如果 ThreadLocal未被外部强引用持有,GC 时会回收 ThreadLocal实例,导致 ThreadLocalMap中的 key 变为 null。
然而,ThreadLocalMap中的 value 仍然是强引用,即使 key 被回收,value 也不会被自动清理。这会导致 ThreadLocalMap中存在大量 key 为 null但 value 仍占用内存的条目,从而引发内存泄漏。
线程复用与内存泄漏的关联
ThreadLocal的变量生命周期与线程绑定。在线程池场景下,线程通常会被复用,导致线程的生命周期可能极长(甚至与 JVM 生命周期一致)。如果未及时清理 ThreadLocal变量(如未调用 remove() 或替换值),其存储的数据会一直堆积。
ThreadLocal与Thread,ThreadLocalMap的关系
图片来自于史上最全ThreadLocal 详解(一)-CSDN博客。从图中可以看出三者之间的关系。
关于ThreadLocal的常见面试题
4.1 ThreadLocal 和 synchronized 的区别?
对比项 | ThreadLocal | synchronized |
---|---|---|
数据隔离方式 | 每个线程独立副本 | 共享数据 + 锁 |
性能 | 无锁,更高性能 | 有锁,可能阻塞 |
适用场景 | 线程隔离数据(如 Session) | 线程共享数据(如计数器) |
4.2 ThreadLocal 的 key 为什么是弱引用?
-
防止内存泄漏:
-
如果 ThreadLocal被回收,Entry 的 key 会自动被 GC 清理,避免 ThreadLocal无法回收。
-
-
但 value 仍需手动清理:
-
因为 value 是强引用,需要 remove() 或 set(null)。
-
4.3 ThreadLocal 的 key 为什么不设为强引用?
在业务代码中使用完ThreadLocal后,即使ThreadLocal引用被回收,由于ThreadLocalMap的Entry强引用了ThreadLocal(ThreadLocal作为key),导致ThreadLocal对象无法被回收。当没有手动删除Entry且当前线程仍在运行时,会形成一条强引用链:当前线程引用→当前线程→ThreadLocalMap→Entry。这个Entry包含了ThreadLocal实例和value,因此不会被回收,从而造成内存泄漏。换言之,由于ThreadLocalMap的key采用强引用机制,内存泄漏问题无法完全避免。
以上就是关于ThreadLocal的一些解析与想法。