三个问题彻底搞懂 ThreadLocal
问题一:什么是ThreadLocal,它的实现原理是什么?
ThreadLocal
是 Java 并发中一个非常重要的类,是用来解决Java多线程程序中并发问题的一种途径。它为每个使用该变量的线程提供一个独立的变量副本,因此每个线程都可以独立地改变自己的副本,而不会影响其他线程中的副本,线程之间的竞态条件被彻底消除了。
ThreadLocal
的核心原理是为每个线程提供一个独立的变量副本,从而实现线程之间的数据隔离。这是通过在每个线程内部维护一个名为 ThreadLocalMap
的映射来实现的,其中键是 ThreadLocal
对象,值是线程特有的变量副本。这样,每个线程只能访问和修改自己的变量副本,而不会干扰到其他线程。为了避免内存泄漏,ThreadLocalMap
使用弱引用作为键,所以我们在使用的时候需要手动清理 ThreadLocal
变量。
ThreadLocal 实现原理详解
Thread、ThreadLocal、ThreadLocalMap 三者之间的关系
要掌握 ThreadLocal
的核心原理,第一要务就是要厘清楚 Thread、ThreadLocal、ThreadLocalMap 三者之间的关系。
Thread 里面有一个 ThreadLocalMap 的成员变量 threadLocals:
ThreadLocal.ThreadLocalMap threadLocals = null;
从这里可以看出,每个线程都有自己的 ThreadLocalMap。ThreadLocalMap 是 ThreadLocal 的静态内部类:
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
ThreadLocalMap 内部维护的是一个或者多个Entry<k,v>
,每个 Entry 的 key 是ThreadLocal 实例的弱引用,value 则线程的专属变量。关系如下:
我们看下 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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
操作顺序是这样的:ThreadLocalMap map = thread.threadLocals
—> return map.getEntry(threadLocal)
。这里我们可以再次进一步理解他们的关系:
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 map = thread.threadLocals
—> map.set(threadLocal, value)
,再次精进他们的关系:
源码及使用方法大明哥就不阐述了,ThreadLocal 的源码很简单,重点在于理解 Thread、ThreadLocal、ThreadLocalMap 之间的关系。
问题二:ThreadLocal为什么会导致内存泄漏?如何解决的?
ThreadLocal 的实现原理:每一个 Thread 维护一个 ThreadLocalMap,key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本,这些对象之间的引用关系如下:
实心箭头表示强引用,虚心箭头表示弱引用
ThreadLocal 的内存泄露发生在 Entry 上,我们现在来详细分析 Entry。
对于 Entry 的 key 来说,它是 ThreadLocal 对象,它有两个引用源,一个是栈内存上的 ThreadLocal Ref,一个是 Entry 中的 key,如下:
对于 Entry 的 value 而言它就只有一条引用链:
对于 Entry 来说,由于存在 key 和 value 两个引用路径,所以这里就会有两种情况:
- 栈上的
ThreadLocal Ref
不再使用了,但是由于 ThreadLocal 对象还有一条引用链存在,这就会导致它无法被回收,时间久了就会导致内存泄露。 - 由于线程池的存在,会让线程一直被重复利用,就会导致第二条 value 链一直存在,导致 ThreadLocalMap 无法被回收,从而导致内存泄露。
内存泄露的解决方案
第一种情况:弱引用
栈上的
ThreadLocal Ref
不再使用了,但是由于 ThreadLocal 对象还有一条引用链存在,这就会导致它无法被回收,时间久了就会导致内存泄露。
为了解决这种情况,ThreadLocal 使用了弱引用。即上图用虚线表示的部分。源代码如下:
tatic class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
对 JVM 熟悉的小伙伴知道,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。
当堆栈上的 ThreadLocal Ref
不再使用了,ThreadLocal 对象就只有弱引用了,那么 ThreadLocal 对象就可以在下次GC时被回收掉了。
第二种情况:使用完 ThreadLocal 后调用 remove()
由于线程池的存在,会让线程一直被重复利用,就会导致第二条 value 链一直存在,导致 ThreadLocalMap 无法被回收,从而导致内存泄露。
value 的生命周期与 Thread 是一样的,由于线程池的存在,导致线程一直都被重复利用,从而导致 value 对象一直都无法被释放。那怎么解决呢?
我们知道 ThreadLocal 本身其实是不存储数据的,数据都是存在 ThreadLocalMap 中的,所以我们在每次调用ThreadLocal的get()
、set()
、remove()
方法的时候,内部实际会调用ThreadLocalMap的get()
、set()
、remove()
操作。而 ThreadLocalMap 的这些方法都会清理key为null,但是value还存在的Entry。
所以,当我们在一个 ThreadLocal 用完之后,可以手动调用一下remove(),就可以在下一次GC的时候,把Entry清理掉。
总结
由于 Thread 中包含了 ThreadLocalMap 的变量,因此 ThreadLocalMap 与 Thread 的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
但是,由于 ThreadLocal 使用了弱引用,则多了一层保障:弱引用 ThreadLocal 不会内存泄漏,因为它会在下一次 GC 时被回收。
ThreadLocal 内存泄漏的根源是:Thread 被重复利用,导致 value 强引用链一直存在,而导致内存泄露,注意,不是因为弱引用。
所以,当我们用完一个 ThreadLocal 后,可以手动调用一下remove(),就可以在下一次GC的时候,把Entry清理掉。
为什么我们在使用 ThreadLocal 的时候,一再强调要手动调用 remove()
方法。
问题三:ThreadLocal的应用场景有哪些?
回答
ThreadLocal
为 Java 并发安全提供了一种新的思路,它采用线程隔离的机制,每个线程都有拥有自己独立的 ThreadLocal
副本,线程之间互不干扰,每个线程都可以独立地、安全地操作这些变量,而不会影响其他线程。
ThreadLocal
在实际工作中还是有比较多的应用场景,典型的有如下几个:
- 数据库连接或会话信息:很多 ORM 框架,如 Hibernate、Mybatis,都是使用
ThreadLocal
来存储和管理数据库会话的。这样就可以每个线程都有自己独立的的数据库连接,避免了多线程之间的数据库连接冲突。 - 用户身份认证:用户登录成功后,一种常规的做法是是将用户信息存储在 Session 中,如果我们需要获取用户的登录信息,就需要先通过 HttpServletRequest 获取到 Session,然后才能通过 Session 获取到用户信息,同时我们需要在每一个需要用户信息的接口都要加上这个参数,这种方式就显得稍微麻烦了点。换一种思路,我们是否可以将用户的登录信息存储在 ThreadLocal 中呢?当用户登录成功后,我们将用户信息存入到 Token 中返回给前端,当用户调用需要授权的接口时,在 header 中携带 Token ,然后在拦截器中解析 Token,获取用户信息,最后将其存入到 ThreadLocal 中,在使用时我们只需要调用工具类的
get()
就可以获取到用户信息,非常方便,需要注意当请求结束后,记得调用remove()
。 - PageHelper 分页:我们在使用 PageHelper 进行分页时,会用这个方法
PageHelper.startPage(page, limit);
,这个方法就是页码和页码大小等相关信息存储在 ThreadLocal 中,然后在执行分页时读取这些信息。 - 请求追踪(TraceId):在分布式系统中,一个请求可能会跨越多个服务,为了追踪这个请求的整个处理流程,通常会生成一个唯一的 traceId,并在处理请求的每个环节中传递这个 traceId。我们使用
ThreadLocal
存储 traceId ,可以在一个服务的处理过程中保持traceId
不变,即使这个处理过程跨越多个线程或方法调用。 - 日志记录:类似于请求追踪,
ThreadLocal
可以用来在日志记录中保持一些上下文信息(如用户ID,请求ID等),这样可以在整个请求处理流程中轻松地将这些上下文信息添加到日志中,而不需要在每个方法调用中传递这些信息。比如大明哥就喜欢给所有请求都加一个业务流水号 businessNo,然后将其存储在 ThreadLocal,便于打印在日终中,这样就可以通过这个 businessNo 跟踪整个请求日志相关信息了,非常方便。
- 点赞
- 收藏
- 关注作者
评论(0)