基本介绍及使用场景
ThreadLocal提供了一个线程本地的变量,在线程中都可以访问到该变量。一般来说threadlocal是一个private static的变量,这样方便多个线程使用。常用的场景:
- 1. 在线程中传递参数,这样不用在调用链的每个方法都传递,eg: transaction id,user id等
- 2. 每个线程一个持有一个副本,避免线程安全的问题
基本使用方法
private static ThreadLocal<Long> userIdThreadLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return -1L;
}
};
然后在不同的线程中使用userIdThreadLocal:
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
userIdThreadLocal.set(100L);
try {
Thread.sleep(3 * 100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1 :" + userIdThreadLocal.get());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread2 :" + userIdThreadLocal.get());
userIdThreadLocal.set(200L);
System.out.println("Thread2 :" + userIdThreadLocal.get());
}
});
thread1.start();
thread2.start();
输出结果为: Thread2 :-1 Thread2 :200 Thread1 :100
内部原理
看下调用过程:
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();
}
ThreadLocal的get()方法,会直接获取当前线程,然后获取当前线程的threadlocals变量,因此threadlocalMap其实是存在thread内部的。 进一步看下ThreadLocalMap的结构,内部是一个Entry数组,key是ThreadLocal,value是线程要保存的值。因此一个Thread是可以有持有多个ThreadLocal变量的,就是存在ThreadLocalMap中。 ThreadLocal.get() -> Thread.threadLocals -> ThreadLocalMap.getEntry 逐个判断数组中Entry的可以是否为当前要找的ThreadLocal。
内存泄漏问题
首先界定下什么是内存泄露。内存泄露是指一块内存不再使用了,但是永远不会被回收。一般说起内存泄露,大家都会跟OOM联系起来,其实不一定引起OOM,如果泄露的内存占整体内存的比例很小,或者不是持续性的,那么并不会导致OOM,因为可用内存还多着呢。
ThreadLocal的内存泄露是如何产生的呢?如果Thread执行完毕并退出,那么对应的ThreadLocalMap对象以及其中的Entry都会被回收,没有内存泄漏的问题。如果是线程一直运行,但是threadLocal中保存的value对象已经不再需要了,此时对象没有用但是一直没有被回收,产生内存泄漏。下图为内存引用关系:
如何避免内存泄露呢?
- 如果将外部的static userIdThreadLocal变量赋为null,结合WeakReference是否就可以避免内存泄漏呢? 答案是不行。因为Entry中Key是软引用,如果外部引用断开后,Entry中的Key会被回收,但是value对象并不会回收,而且永远也访问不到,因为key变成了Null。
- 最佳实践是如果不再使用,手动的调用remove方法。
- 调用ThreadLocal的get,set 等方法时也会触发调用 expungeStaleEntry 方法,清理掉threadlocal已经为Null的value对象。
父子线程问题
实际应用中可能出现子线程需要使用父线程的threadlocal变量的情况,此时直接threadlocal是不可继承的。JDK已经考虑了这种情况,可以使用InheritableThreadLocal,子线程可以继承父线程的threadlocal。用法同ThreadLocal完全一样。完美?那看下是怎么实现的,下面是Thread的init方法中有关的一部分:
`
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
`
这个实现方式决定了只有每次使用新线程都new一个出来,才能共享父线程的inheritThreadLocals,实战中大部分场景用到的都是线程池,那就会出现只有第一次使用线程池时inheritThreadLocals中的值是正确的,之后就不会再改变。显然是有问题的。
线程池中如何正确使用ThreadLocal
常用的方式:
- 在提交任务到线程池时继承Runnable或者Callable类,手动将需要的参数传递进去。这种方式一定程度上违背了我们使用ThreadLocal的初衷,我们就是不想到处传递参数才用ThreadLocal的。
- 使用Spring的ThreadPoolTaskExecutor,该方法提供了setTaskDecorator方法,设置一个装饰器,我们可以在装饰器中做上面 inheritThreadLocals 赋值的操作
- 使用阿里的TransmitThreadLocal 最好的方式。
阿里的TransmitThreadLocal
TransmitThreadLocal常用的两种用法,
- 使用其提供的TtlRunnable、TtlCallable
- 使用TtlExecutors提供的getExecutor,getExecutorService,getScheduledExecutorService等方法 其本质都是,在创建任务类时拷贝父线程的threadlocal,任务执行完成后做清理。 用法比较简单,可以直接点开参考资料中的UserGuide在此不多赘述。
欢迎大家多多讨论,批评指正,如果您觉得不错,点赞转发关注走起。 参考资料: TransmitThreadLocal GitHub地址