ThreadLocal对于Java程序员来说一定不陌生,作为多线程编程最常用的类经常被使用到,面试中也经常考到。
下面从以下几个方面彻底搞懂ThreadLocal:
维护调用链路的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类源码,提供常用的如下几个方法:
public T get() { }
public void set(T value) { }
public void remove() { }
通过查看get方法,可以看出,访问的是当前Thread维护的threadLocals属性,每个Thread都有一份ThreadLocal.ThreadLocalMap副本。所以这也就是为什么ThreadLocal能保证修改和获取的是当前线程的数据。
ThreadLocalMap也是一个Map,Key是当前调用该方法的ThreadLocal对象,value是set的泛型值。
// 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是弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
当有人问你使用ThreadLocal需要注意什么的时候,其实就是想问你关于ThreadLocal的内存泄漏问题。
因为ThreadLocalMap的Key是弱引用的,在GC时会回收掉。当线程的生命周期大于ThreadLocal的生命周期时(大部分情况都是的,因为线程通过线程池管理会重复利用),那么就可能存在ThreadLocalMap
的情况(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在),这个Object就是泄漏的对象。
可以做个实验,重现内存泄漏问题。执行如下代码:
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日志:
-Xmx50m
-XX:+PrintGCDetails
最后通过控制台发现,程序执行触发了多次Young GC 和 Full GC,最后出现内存溢出:java.lang.OutOfMemoryError: Java heap space
。
为了避免这个问题,我们可以在线程执行退出前,执行ThreadLocal的remove方法,即移除ThreadLocalMap中当前ThreadLocal对应的数据。方法源码:
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