彻底搞懂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类源码,提供常用的如下几个方法:

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原理_第1张图片

关于内存泄漏

当有人问你使用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

你可能感兴趣的:(彻底搞懂ThreadLocal原理)