ThreadLocal使用比较常见,但是一个觉得这个东西哪里怪怪的,给人的感觉不是特别直观,本文通过一个常见使用场景,来分析其来龙去脉。
1.定义&作用
定义: ThreadLocal叫做线程本地变量,顾名思义,就是Thread的一个内部变量,这个ThreadLocal是属于某个线程的。就好比是某个人的老婆(们),老婆只能属于某个人,而不能被共享;这里老婆有可能是复数,一个人可以有多个老婆(好比在古代),老婆跟着丈夫走,丈夫生而生,丈夫死而死;
作用: 既然是属于某一个线程的变量,那么肯定就不是被多个线程共享的;这个东西在多线程场景中使用,目的就是防止变量在多个线程中游走,产生线程安全的问题。
下面描述一个场景:
在web项目中,比如常用的springmvc项目,每次请求服务端,tomcat都会使用一个单独的线程来处理这个请求。假如两个用户,用户A和用户B,都打开了一款app进入首页,假设首页数据是一个接口返回的,那么这两个用户就分别发起了一次请求,对服务端来说,就是两次请求。
这两个请求,同时来到后台服务端,是两个单独的线程threadA和threadB来处理的。假设首页的数据需要使用用户信息,比如userId(用户id),accountId(账户id),有很多业务方法都需要这个userId,accountId。比如进入首页的controller方法后,又依次调用了A.a()–>B.b()–>C.c()方法,最终将数据返回,但是a(),b(),c()这3个方法,都需要userId,accountId,这个时候userId,accountId该怎样获取呢?
这里简单一点,假设前端传进来了userId,我们还不知道accountId,这个时候有两种处理方式:
- 1.在a(),b(),c()中用到accountId的时候,都使用userId去数据库(或者缓存啥的)中查询一次;这样可以完成任务,但是有两次查询时多余的,增加了接口耗时,这时如果业务增加了d/e/f方法也要用到accounId,那么就要增加更多次不必要的查询。
- 2.使用ThreadLocal。我们可以在a()方法(当然更好的是在a之前,比如增加拦截器,在拦截器中查询)查询到accountId后,就放到当前线程的threadLocal中存储,这样当此线程的业务往下执行到bcdf方法时,如果需要accountId,直接从线程的threadLoca变量中查询即可。
2.使用Demo
说到这里,ThreadLocal的使用场景,就基本明确了,针对这种场景,下面上个Demo直接感受一下:
public class ThreadLocalDemo {
public static void main(String[] args) {
//起两个线程,模拟两个用户发起请求到web服务,
// 比如两个用户都在请求首页接口
//这时web服务端会有两个线程对应处理两个请求
for (int i = 0; i < 2; i++) {
int fn = i;
new Thread(() -> {
ServiceA.a("accountId" + fn);
ServiceB.b();
ServiceC.c();
}, "线程_" + i).start();
}
}
/**
* 一般使用一个全局静态类管理ThreadLocal对象
*/
static class ResourceClass {
public final static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
public final static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
public static void setThreadLocal1(String value) {
ResourceClass.threadLocal1.set(value);
}
public static String getThreadLocal1() {
return ResourceClass.threadLocal1.get();
}
public static void setThreadLocal2(String value) {
ResourceClass.threadLocal2.set(value);
}
public static String getThreadLocal2() {
return ResourceClass.threadLocal2.get();
}
}
/**
* 业务类A
*/
static class ServiceA {
/**
* 模拟业务,获取到accountId,并保存到threadLocal1变量中
* 同时保存线程名字到threadLocal2中
*/
public static void a(String accountId) {
String name = Thread.currentThread().getName();
ResourceClass.setThreadLocal1(accountId);
ResourceClass.setThreadLocal2(name);
System.out.println(name + "在A类中保存accountId: " + accountId + " 到ThreadLocal\n");
}
}
/**
* 业务类B
*/
static class ServiceB {
/**
* 模拟执行一些业务,要用到accountId
*/
public static void b() {
String accountId = ResourceClass.getThreadLocal1();
String name = ResourceClass.getThreadLocal2();
System.out.println(name + "在B类利用accountId:" + accountId + " 继续执行一些B类业务\n");
}
}
/**
* 业务类C
*/
static class ServiceC {
/**
* 模拟执行一些业务,要用到accountId
*/
public static void c() {
String accountId = ResourceClass.getThreadLocal1();
String name = ResourceClass.getThreadLocal2();
System.out.println(name + "在C类利用accountId:" + accountId + " 继续执行一些C类业务\n");
}
}
}
结果:
线程_1在A类中保存accountId: accountId1 到ThreadLocal
线程_0在A类中保存accountId: accountId0 到ThreadLocal
线程_0在B类利用accountId:accountId0 继续执行一些B类业务
线程_1在B类利用accountId:accountId1 继续执行一些B类业务
线程_0在C类利用accountId:accountId0 继续执行一些C类业务
线程_1在C类利用accountId:accountId1 继续执行一些C类业务
上述Demo中,
- 我们在main方法中开了两个线程来模拟处理前端的两个请求;线程_0保存的是accountId0,得到的也是accountId0;线程_1保存的是accountId1,得到的也是accountId1;最终也可以说明,threadLocal在多个线程中是独立不相互影响的;
- ResourceClass中,我们new了两个threadLocal对象,一般我们使用一个threadLocal对象即可(里面存map),这里主要像说明Thread类中的threadLocals变量是支持存储多个threadLocal对象的;
- 使用的多个业务类,是为了说明threadLocal的生命周期是跟着线程走的,在线程中的任何地方都可以获取到线程本地变量中的内容。
3.源码分析
下面直接看源码,
由于ThreadLocal叫做线程本地变量,那么说明就是Thread.java的一个内部属性,打开Thread看下:
在Thread类中,我们可以找到如上信息,变量名:threadLocals,复数说明一个线程可以有多个threadLocal,也就是说ThreadLocal.ThreadLocalMap可以保存多个ThreadLocal,那么我们打开看看是啥:
是ThreadLocal一个内部类,仔细看过其内容后,发现ThreadLocalMap就是个map,跟Hashmap类似,这里是另外一种map。
这里ThreadLocalMap是内部类,其实我们可以认为它就是一个map,和ThreadLocal类没有任何关系。因为ThreadLocal类只不过是一个工具类,提供了若干方法,来操作Thread类中的threadLocalMap对象。
ThreadLocalMap中有个内部类Entry ,因为是map,Entry类就是用来key和value值的,源码如下:
//继承了弱引用WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//构造函数
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
key是ThreadLocal对象本身,在本文的Demo中,就是threadLocal1和threadLocal2指向的对象(new 的 ThreadLocal 对象),value就是accountId。
Entry继承了弱引用WeakReference(弱引用指向的对象在GC时会被回收,如果不清楚弱引用,可参考文章《弱引用WeakReference作用与使用场景》),使用弱引用的目的,咱们结合本文demo来说。同时参考如下一张关系图:
我们把本文demo和上图联系起来:
- Heap中的ThreakLocal对象:就是本demo中new的两个threadLocal对象;
- ThreadLocalRef :就是demo中的threadLocal1和threadLocal2,它与heap中的threadLocal1(threadLocal2)之间是实线,表示强引用;
- key: 因为Entry是继承了弱引用,key保存的也是new的threadLocal对象,但他们关系是弱引用,虚线表示;
- value:本demo中就是accountId;
- CurrentThreadRef:本文demo中启了两个线程。线程_0和线程_1,CurrentThreadRef就是指向线程对象的,属于操作系统层面;
- CurrentThread:对应demo中的线程_0和线程_1;
- Map:线程_0和线程_1中都有个map对象,就是ThreadLocalMap类型的那个;
然后我们再说为啥ThreadLocalMap中的Entry为啥是弱引用,
因为当上图中ThreadLocalRef,也就是threadLocal1和threadLocal2假如被赋值了null(threadLocal1=null),这个时候heap中的ThreakLocal对象理论上应该成为垃圾被GC回收,但是,ThreakLocal对象此时还被key引用着呢,如果之间是强引用关系,ThreakLocal对象就不会被回收,但实际上业务又用不着这个对象了,这就产生了不能被回收的对象,造成内存泄漏;为了避免这个,只能使用弱引用。
当然使用弱引,虽然避免了ThreakLocal不能被回收,但是上图中的value值还在内存中,只要线程不销毁(比如线程池中的核心线程),那么map就一直存在,map中的Entry对象就一直存在,那么Entry中value对象就一直存在,依然会有内存泄漏的问题,因此,我们使用完map中的Entry对象时,要使用ThreadLocal提供的remove()方法将其从内存中移除,下面就让我们继续分析ThreadLocal类提供的几个常用方法。
下面看下ThreadLocal工具类中的几个常用方法:
3.1 set(T value)
public void set(T value) {
//得到当前线程对象
Thread t = Thread.currentThread();
//从当前线程中get到内部变量ThreadLocalMap 类型的map
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map不是空,那么就直接把value Set进去,key是当前ThreadLocal对象
map.set(this, value);
else
//如果map是空,就初始化,创建一个map
createMap(t, value);
}
上面方法逻辑比较简单,直接看注释。其中有几个方法,需要打开看下:
- getMap(t):
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
就是返回当前Thread线程对象中的threadLocals变量。
- map.set(this, value)
ThreadLocalMap的set方法,
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//计算value在map数组中的插入位置
int i = key.threadLocalHashCode & (len-1);
//从下标i位置开始遍历找合适的位置 插入
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果map中已经存在要插入的key,那么就直接覆盖key对应的value
if (k == key) {
e.value = value;
return;
}
//如果map中,下标i位置的元素key是空的,说明,key已经被GC回收掉了,那么就将当前要插入的key和value放到这里,将旧的
//value值替换掉
if (k == null) {
//清理无用的旧值
replaceStaleEntry(key, value, i);
return;
}
}
//如果map中下标i位置的元素直接是空的,那么就将当前要插入的key和value放到这里
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果map的大小达到了阈值
rehash();
}
方法总结:
- ThreadLocal的set方法,就是往当前线程中存储key和value,key就是当前正在使用的ThreadLocal对象;value就是要存取的业务值,比如本demo中的accountId;
- 如果要插入的key已经存在,那么使用新的value替换掉老的value;
- 如果有Entry的key指向的对象已经是null(弱引用,对象被GC了),那么就插入key和value,同时清理一遍这种无用的旧值。
- 如果上面两种情况不存在,比如第一次插入,那么就直接new一个Entry对象插入即可;
- 最后要判断map的Entry数组容量,超过阈值就扩容,类似Hashmap那一套,不深入分析了。
3.2 get()
public T get() {
//获取到当前线程
Thread t = Thread.currentThread();
//取出线程种的本地变量map
ThreadLocalMap map = getMap(t);
if (map != null) {
//知道当前threadLocal对象的key对用的Entry节点
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//得到Entry节点中的value值
T result = (T)e.value;
return result;
}
}
//如果还没初始化线程本地变量,也就是还没存过值,那么就进行初始化
return setInitialValue();
}
//和上面set()方法初始化一样
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;
}
get()方法总结:
- 从当前线程中得到本地变量map对象
- map中可能有多个节点,比如本demo中,每个线程的threadLocalMap对象中有两个节点,即两对key和value,key分别是threadLocal1和threadLocal2;如果业务中使用的threadLocal1.get()方法,那么就是获取key为threadLocal1的map节点entry。
- 找到entry,就返回其中的value,比如demo中的accountId;
- 当然,如果get时发现线程中的threadLocalMap还是空的,也就是还没set进去过任何东西,那么就先初始化。
3.3 remove()
上文分析的时候,提到过使用threadLocal可能会产生内存泄漏,但是当在业务中使用完threadLocal,要使用remove()方法将其从内存删除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//删除当前threadLocal对象的key和其对应的value,也就是从map中删除一对key和value
m.remove(this);
}
关键方法是m.remove(this),这个是ThreadLocalMap类中的方法,继续跟进,
/**
* Remove the entry for key.
*/
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) {
//这里清楚,就是让此节点等于null即可
e.clear();
//整理一遍map数组,清楚key已经是null的节点
expungeStaleEntry(i);
return;
}
}
}
内容比较简单,就是遍历map数组,根据key找到对应的entry,然后将其值设置为null即可。
本文就写到这里,综上,我们关键还是要结合demo理解ThreadLocal的使用场景,以及具体的运作流程。