一.ThreadLocal介绍
ThreadLocal在日常工作中的使用频率还是很高的,但是大多使用场景比较单一,而且使用不当的情况下,会出现内存泄露的情况。
那么ThreadLocal到底是什么,我们先来看一下官方类注释。
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
*/
复制代码
ThreadLocal提供了线程的局部变量,每个线程都可以通过set()
和get()
方法操作此变量,并且不会与其他线程的局部变量进行冲突,保证了线程间局部变量的隔离行。
大白话:ThrealLocal类型的变量属于当前线程,哪怕是同一个方法内的同一个ThreadLocal变量,他们的值在不同的业务运行下也是不一样的,线程安全。
二.ThreadLocal业务应用
2.1.维护数据库连接
jdbc时代,数据库连接connection需要我们手动来维护,同一个库连接我们可以用数据库连接池来实现。但是不同数据库连接的时候,不同的线程需要获取到不同的连接。这时候就可以使用ThreadLocal来维护线程池中不同数据源连接。
demo我太懒就不贴了
贴一个网上人家实现的mybatis动态多数据源的实现
2.2.全局变量传递
业务开发中,对于当前请求用户数据的获取是一个很常见的需求。比如下订单的场景,我通过请求的token解析获取到用户数据,那用户数据可能在常见的MVC三层中都会被使用到。比较麻烦的方法是,在每个方法的入参都维护一个User参数进行传递。这样做的劣势在于,当前业务场景所关联的所有方法都多了一个User的入参。
因为每个线程对应的用户信息可能都是不一样的,但是针对于一个token而言,用户信息的维护都是单一的。那么可以通过拦截器/过滤器/AOP在controller层解析token获取到User信息,放入ThreadLocal变量中,让当前线程相关的所有方法共享此变量,减少了,方法参数的传递。
2.3.链路追踪
微服务架构下,多个服务之间调用如果出现了报错,使用链路追踪是一个常见的手段。在请求的线程变量中嵌入traceId,根据这个traceId就可以找到对应请求分别在各个业务应用的日志。
关于链路追踪up主写过一篇入门级的spring-cloud-sleuth文章,感兴趣的可以看看
三.ThreadLocal原理解析
为了更好的理解ThreadLocal,阅读源码肯定的逃不开的了。面试的时候造个火箭也挺好不是吗?
重点解析一下set(),get(),remove()方法
3.1.set()方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取维护当前线程变量的ThreadLocalMap数据,一种类似于HashMap的数据结构
ThreadLocalMap map = getMap(t);
//如果当前线程已经存在了Map,直接调用map.set
if (map != null)
map.set(this, value);
//不存在Map,则先进行新增map,再进行set
else
createMap(t, value);
}
复制代码
set方法中出现了一个ThreadLocalMap这个数据结构,点进去看一下
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//代码太多不一一贴出来了
}
复制代码
其中维护了一个entry结构用来用来维护节点的数据,细心地同学应该已经发现了Entry这个结构继承了WeakReference,从构造方法可以看出,ThreadLocalMap的Key是软引用维护的。这个地方很重要,至于为什么重要,后面再细说。
再继续点击一下发现ThreadLocal成员变量里面定义了这么一句话
ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码
这句话的出现表明了,针对于每一个线程,都是独立维护一个ThreadLocalMap,一个线程也可以拥有多个ThreadLocal变量。
3.2.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();
}
复制代码
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
复制代码
protected T initialValue() {
return null;
}
复制代码
get()方法整体上比较简单,贴上了关键逻辑逻辑代码,调用get()时,如果存在值,则将值返回,不存在值调用setInitialValue()获取值,其中初始化的值为null,也就是说如果ThreadLocal变量未被赋值,或者赋值后被remove掉了,直接调用get()方法不会报错,将会返回null值。
3.3.remove()方法
//ThreadLocal
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
复制代码
//ThreadLocalMap
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
复制代码
remove方法调用时会判断当前线程中ThreadLocalMap是否存在,如果存在则调用ThreadLocalMap.remove(key);遍历链表结构移除entry节点
3.4.总结
1.Thread维护着一个ThreadLocalMap的引用,其中ThreadLocalMap的key为WeakReference维护
2.ThreadLocal本身并不存储值,ThreadLocal通过操作ThreadLocalMap达到对线程变量的赋值,获取,删除操作。
4.ThreadLocal的内存泄露问题
看一下JVM中对ThreadLocal的堆栈维护图
entry对于value的引用为强应用,key的引用为弱引用
弱引用,强应用概念:blog.csdn.net/zalu9810/ar…
如果一个对象只是被弱引用引用者,那么只要发生 GC,不管内存空间是否足够,都会回收该对象。
那么问题就来了,如果操作ThreadLocal变量的方法QPS很高,疯狂被请求,这个时候你调用了set(),get()方法,并未调用remove方法,那么,当GC发生。entry与ThreadLocal的关联关系中断,Key被回收,value还被强连接关联着。这样跟垃圾回收可达性分析,value仍旧为可达,但是从业务角度上看,这个value值将永远访问不到,出现了内存泄露。
因此在使用ThreadLocal时必须要显示的调用remove方法,否则出现了问题,排查起来都很麻烦。
5.InheritableThreadLocal
日常工作中不可能所有工作都是基于单线程操作的。那在多线程情况下,主线程中定义的ThreadLocal变量,能在子线程中访问到吗?试一下看看
public class Demo {
public static final ThreadLocal t = new ThreadLocal();
public static void main(String[] args) {
t.set("test");
new Thread(() -> {
System.out.println("new:"+t.get());
}).start();
System.out.println("main:"+t.get());
}
}
输出:
main:test
new:null
复制代码
淦,居然不能,这不凉了。
不要慌,开发java的大神早就考虑到我们的日常业务场景,在父子线程里面传递变量使用了InheritableThreadLocal,ThreadLocal是他的父类。试一下上面的代码
public class Demo {
public static final ThreadLocal t = new InheritableThreadLocal();
public static void main(String[] args) {
t.set("test");
new Thread(() -> {
System.out.println("new:"+t.get());
}).start();
System.out.println("main:"+t.get());
}
}
输出:
main:test
new:test
复制代码
Nice~
我们来看看InheritableThreadLocal到底是何方神圣。
点击去这个类,比较简短,重写了三个ThreadLocal的方法
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
//查看1方法引用,逐层点击,可以发现这个初始化变量到了Thread这个类的init()方法中
protected T childValue(T parentValue) {
return parentValue;
}
//获取map被替换成了inheritableThreadLocals,而非threadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
//创建map的时候赋值使用inheritableThreadLocals,而非threadLocals
void createMap(Thread t, T firstValue) t
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
复制代码
第二个与第三个方法中比较简单,主要为原先使用ThreadLocal维护变量的地方都变成了inheritableThreadLocals。重点讲一下第一个方法,追溯到Thread类中init()方法,其中初始化代码中有这么一句话
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
复制代码
将父线程的的线程Map赋值给了子线程,达到了,父子线程间的通信。
nice~
6.TransmittableThreadLocal
日常工作多线程编程使用直接新建线程的方式毕竟还是少数,更加规范的方式还是使用线程池。那么主线程中如果使用InheritableThreadLocal,主线程中能够传递线程变量进去吗。我们来试试看
public class Demo {
public static final ThreadLocal t = new InheritableThreadLocal();
public static void main(String[] args) {
t.set("test");
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
System.out.println(t.get());
});
System.out.println("main:"+t.get());
}
}
输出
main:test
test
复制代码
成功了,nice~
测一下并发
public class Demo {
public static final InheritableThreadLocal t = new InheritableThreadLocal();
public static void main(String[] args) {
t.set("test");
ExecutorService executorService = Executors.newFixedThreadPool(1);
Runnable runnable1 = () -> {
System.out.println("new修改前:" + t.get());
};
Runnable runnable2 = ()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("new修改后:"+t.get());
};
executorService.submit(runnable1);
System.out.println("main修改前:"+t.get());
t.set("test1");
System.out.println("main修改后:"+t.get());
executorService.submit(runnable2);
System.out.println("main最后:"+t.get());
}
}
输出:
main修改前:test
new修改前:test
main修改后:test1
main最后:test1
new修改后:test
复制代码
这下又懵了,修改后的InheritableThreadLocal变量未被修改,还是原来的值。
我们来回顾一下线程池与新建线程的区别,线程池的核心线程池在为开启释放核心线程池的情况下,将会被重复使用,但是InheritableThreadLocal中变量的维护在新建线城时才会进行一次赋值,所以出现了修改之后,新提交的任务,无法获取更新的InheritableThreadLocal变量值。
那这个问题怎么解决呢? TransmittableThreadLocal来帮你解决。这个是阿里开发的一种三方库,专门用来解决线程池内线程变量通信的问题,这里具体的原理不做展开解析了,贴上官网介绍
我们来使用一下
public class Demo {
public static final ThreadLocal t = new TransmittableThreadLocal();
public static void main(String[] args) {
t.set("test");
ExecutorService executorService = Executors.newFixedThreadPool(1);
Runnable runnable1 = () -> {
System.out.println("new修改前:" + t.get());
};
Runnable runnable2 = ()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("new修改后:"+t.get());
};
executorService.submit(TtlRunnable.get(runnable1));
System.out.println("main修改前:"+t.get());
t.set("test1");
System.out.println("main修改后:"+t.get());
executorService.submit(TtlRunnable.get(runnable2));
System.out.println("main最后:"+t.get());
}
}
输出:
main修改前:test
new修改前:test
main修改后:test1
main最后:test1
new修改后:test1
复制代码
nice~
7.总结
本文重点为大家介绍了ThreadLocal在各个日常业务开发场景下的应用,同时拓展介绍了InheritableThreadLocal在父子线程间通信的原理与方式,最后引入了阿里的TransmittableThreadLocal来支持主线程与线程池之间的线程变量通信,希望能让大家对ThreadLocal有一个系统的认知与帮助。
8.参考
9.联系我
钉钉:louyanfeng25
微信:baiyan_lou