彻底搞懂 ThreadLocal 原理

ThreadLocal 对于 Java 程序员来说一定不陌生,作为多线程编程最常用的类经常被使用到,面试中也经常考到。

下面从以下几个方面彻底搞懂 ThreadLocal:

  1. 应用场景

  2. 实现原理

  3. 关于内存泄漏

使用场景

维护调用链路的 requestID

在分布式系统中,一个面向用户的服务往往由内部系统多次调用组成。如:下单,用户点击提交订单,订单前置服务接收到请求后,还需要调用其它系统才能完成操作,包括:购物车系统、库存系统、订单系统、支付系统。它们之间的请求是有上下文关联的。

常见的可以使用 requestID 来串起来。订单前置服务接收到请求后,生成一个唯一的 requestID 并保存到 ThreadLocal 中,RPC 框架在调用的时候,从 ThreadLocal 中拿到最开始设置的 requestID,并传给下游。下游接收到请求后同样设置到 ThreadLocal 中。这样当要定位问题时大家直接找 requestID 就可以了。

Log4J 的 MDC

系统会打印日志,比如一个请求过来到处理结束会打印 100 行。但是系统是并发的,这 100 行日志可能是穿插在别的请求一起。那么如何快速定位到指定请求的日志呢?

Log4J 里面有个 MDC 和 NDC 功能,只要调用 MDC.put 方法,设置一个唯一的 traceId,在 Log4J 打印格式 pattern 配置中添加[%X],则每行都会打印这个 traceId。内部使用的也是 ThreadLocal。

MyBatis 插件 PageHelper

PageHelper 这个插件很好用,省去分页查询需要手工添加查询 count 的语句。其内部也用到了 ThreadLocal 保存分页信息。参考类:PageMethod

实现原理

查看 ThreadLocal 类源码,提供常用的如下几个方法:

1
2
3
public T get() { }
public void set(T value) { }
public void remove() { }

通过查看 get 方法,可以看出,访问的是当前 Thread 维护的 threadLocals 属性,每个 Thread 都有一份 ThreadLocal.ThreadLocalMap 副本。所以这也就是为什么 ThreadLocal 能保证修改和获取的是当前线程的数据。

ThreadLocalMap 也是一个 Map,Key 是当前调用该方法的 ThreadLocal 对象,value 是 set 的泛型值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

// 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
}

// Thread 类代码
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

但是这个 Key 有点特殊,ThreadLocalMap.Entry 继承的是 WeakReference,即这个 ThreadLocalMap 的 Key 是弱引用。

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

img

关于内存泄漏

当有人问你使用 ThreadLocal 需要注意什么的时候,其实就是想问你关于 ThreadLocal 的内存泄漏问题。

因为 ThreadLocalMap 的 Key 是弱引用的,在 GC 时会回收掉。当线程的生命周期大于 ThreadLocal 的生命周期时(大部分情况都是的,因为线程通过线程池管理会重复利用),那么就可能存在 ThreadLocalMap<null, Object> 的情况(ThreadLocal 被回收,ThreadLocal 关联的线程共享变量还存在),这个 Object 就是泄漏的对象。

可以做个实验,重现内存泄漏问题。执行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {

private static ThreadLocal<byte[]> MY_LOCAL = new ThreadLocal();

public static void main(String[] args) {
useThreadPoolModel();
}

// 使用线程池方式循环 100 次调用 ThreadLocal 的 set 方法。
private static void useThreadPoolModel() {
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
MY_LOCAL.set(new byte[1000 * 1024]);
}
});
}
executorService.shutdown();
}
}

为了使实验效果更明显,改小 JVM 的内存大小,并打印 GC 日志:

1
2
-Xmx50m
-XX:+PrintGCDetails

最后通过控制台发现,程序执行触发了多次 Young GC 和 Full GC,最后出现内存溢出:java.lang.OutOfMemoryError: Java heap space

为了避免这个问题,我们可以在线程执行退出前,执行 ThreadLocal 的 remove 方法,即移除 ThreadLocalMap 中当前 ThreadLocal 对应的数据。方法源码:

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

为什么使用弱引用

从表面上看内存泄漏的根源在于使用了弱引用。但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

先来看看官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

下面我们分两种情况讨论:

  • key 使用强引用

    引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。

  • key 使用弱引用

    引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障,弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。

参考文章

https://blog.csdn.net/vicoqi/article/details/79743112

https://www.cnblogs.com/aspirant/p/8991010.html